diff --git a/include/crm/common/Makefile.am b/include/crm/common/Makefile.am
index 4d2055f5d7..a70711b9c1 100644
--- a/include/crm/common/Makefile.am
+++ b/include/crm/common/Makefile.am
@@ -1,52 +1,51 @@
 #
 # 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 		\
 		 nvpair.h 		\
 		 options.h 		\
 		 output.h 		\
 		 resources.h		\
 		 results.h 		\
 		 results_compat.h 	\
 		 roles.h		\
 		 rules.h		\
 		 scheduler.h		\
 		 scheduler_types.h	\
 		 schemas.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/logging.h b/include/crm/common/logging.h
index abc2fe85c0..37983a8415 100644
--- a/include/crm/common/logging.h
+++ b/include/crm/common/logging.h
@@ -1,432 +1,428 @@
 /*
  * 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.
  */
 
 #ifndef PCMK__CRM_COMMON_LOGGING__H
 #define PCMK__CRM_COMMON_LOGGING__H
 
 #include <stdio.h>
 #include <stdint.h>             // uint8_t, uint32_t
 #include <glib.h>
 #include <qb/qblog.h>
 #include <libxml/tree.h>
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Wrappers for and extensions to libqb logging
  * \ingroup core
  */
 
 
 /* Define custom log priorities.
  *
  * syslog(3) uses int for priorities, but libqb's struct qb_log_callsite uses
  * uint8_t, so make sure they fit in the latter.
  */
 
 // Define something even less desired than debug
 #ifndef LOG_TRACE
 #define LOG_TRACE   (LOG_DEBUG+1)
 #endif
 
 // Print message to stdout instead of logging it
 #ifndef LOG_STDOUT
 #define LOG_STDOUT  254
 #endif
 
 // Don't send message anywhere
 #ifndef LOG_NEVER
 #define LOG_NEVER   255
 #endif
 
 /* "Extended information" logging support */
 #ifdef QB_XS
 #define CRM_XS QB_XS
 #define crm_extended_logging(t, e) qb_log_ctl((t), QB_LOG_CONF_EXTENDED, (e))
 #else
 #define CRM_XS "|"
 
 /* A caller might want to check the return value, so we can't define this as a
  * no-op, and we can't simply define it to be 0 because gcc will then complain
  * when the value isn't checked.
  */
 static inline int
 crm_extended_logging(int t, int e)
 {
     return 0;
 }
 #endif
 
 // @COMPAT Make internal when we can break API backward compatibility
 //! \deprecated Do not use
 extern unsigned int crm_log_level;
 
 // @COMPAT Make internal when we can break API backward compatibility
 //! \deprecated Do not use
 extern unsigned int crm_trace_nonlog;
 
 /*! \deprecated Pacemaker library functions set this when a configuration
  *              error is found, which turns on extra messages at the end of
  *              processing. It should not be used directly and will be removed
  *              from the public C API in a future release.
  */
 extern gboolean crm_config_error;
 
 /*! \deprecated Pacemaker library functions set this when a configuration
  *              warning is found, which turns on extra messages at the end of
  *              processing. It should not be used directly and will be removed
  *              from the public C API in a future release.
  */
 extern gboolean crm_config_warning;
 
 void crm_enable_blackbox(int nsig);
 void crm_disable_blackbox(int nsig);
 void crm_write_blackbox(int nsig, const struct qb_log_callsite *callsite);
 
 void crm_update_callsites(void);
 
 void crm_log_deinit(void);
 
 /*!
  * \brief Initializes the logging system and defaults to the least verbose output level
  *
  * \param[in] entity  If not NULL, will be used as the identity for logging purposes
  * \param[in] argc    The number of command line parameters
  * \param[in] argv    The command line parameter values
  */
 void crm_log_preinit(const char *entity, int argc, char *const *argv);
 gboolean crm_log_init(const char *entity, uint8_t level, gboolean daemon,
                       gboolean to_stderr, int argc, char **argv, gboolean quiet);
 
 void crm_log_args(int argc, char **argv);
 void crm_log_output_fn(const char *file, const char *function, int line, int level,
                        const char *prefix, const char *output);
 
 // Log a block of text line by line
 #define crm_log_output(level, prefix, output)   \
     crm_log_output_fn(__FILE__, __func__, __LINE__, level, prefix, output)
 
 void crm_bump_log_level(int argc, char **argv);
 
 void crm_enable_stderr(int enable);
 
 gboolean crm_is_callsite_active(struct qb_log_callsite *cs, uint8_t level, uint32_t tags);
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 /* returns the old value */
 unsigned int set_crm_log_level(unsigned int level);
 
 unsigned int get_crm_log_level(void);
 
 void pcmk_log_xml_as(const char *file, const char *function, uint32_t line,
                      uint32_t tags, uint8_t level, const char *text,
                      const xmlNode *xml);
 
 /*
  * Throughout the macros below, note the leading, pre-comma, space in the
  * various ' , ##args' occurrences to aid portability across versions of 'gcc'.
  * https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html#Variadic-Macros
  */
 #if defined(__clang__)
 #define CRM_TRACE_INIT_DATA(name)
 #else
 #include <assert.h> // required by QB_LOG_INIT_DATA() macro
 #define CRM_TRACE_INIT_DATA(name) QB_LOG_INIT_DATA(name)
 #endif
 
 /*!
  * \internal
  * \brief Clip log_level to \p uint8_t range
  *
  * \param[in] level  Log level to clip
  *
  * \return 0 if \p level is less than 0, \p UINT8_MAX if \p level is greater
  *         than \p UINT8_MAX, or \p level otherwise
  */
 /* @COMPAT: Make this function internal at a compatibility break. It's used in
  * public macros for now.
  */
 static inline uint8_t
 pcmk__clip_log_level(int level)
 {
     if (level <= 0) {
         return 0;
     }
     if (level >= UINT8_MAX) {
         return UINT8_MAX;
     }
     return level;
 }
 
 /* Using "switch" instead of "if" in these macro definitions keeps
  * static analysis from complaining about constant evaluations
  */
 
 /*!
  * \brief Log a message
  *
  * \param[in] level  Priority at which to log the message
  * \param[in] fmt    printf-style format string literal for message
  * \param[in] args   Any arguments needed by format string
  */
 #define do_crm_log(level, fmt, args...) do {                                \
         uint8_t _level = pcmk__clip_log_level(level);                       \
                                                                             \
         switch (_level) {                                                   \
             case LOG_STDOUT:                                                \
                 printf(fmt "\n" , ##args);                                  \
                 break;                                                      \
             case LOG_NEVER:                                                 \
                 break;                                                      \
             default:                                                        \
                 qb_log_from_external_source(__func__, __FILE__, fmt,        \
                                             _level, __LINE__, 0 , ##args);  \
                 break;                                                      \
         }                                                                   \
     } while (0)
 
 /*!
  * \brief Log a message that is likely to be filtered out
  *
  * \param[in] level  Priority at which to log the message
  * \param[in] fmt    printf-style format string for message
  * \param[in] args   Any arguments needed by format string
  *
  * \note This does nothing when level is \p LOG_STDOUT.
  */
 #define do_crm_log_unlikely(level, fmt, args...) do {                       \
         uint8_t _level = pcmk__clip_log_level(level);                       \
                                                                             \
         switch (_level) {                                                   \
             case LOG_STDOUT: case LOG_NEVER:                                \
                 break;                                                      \
             default: {                                                      \
                 static struct qb_log_callsite *trace_cs = NULL;             \
                 if (trace_cs == NULL) {                                     \
                     trace_cs = qb_log_callsite_get(__func__, __FILE__, fmt, \
                                                    _level, __LINE__, 0);    \
                 }                                                           \
                 if (crm_is_callsite_active(trace_cs, _level, 0)) {          \
                     qb_log_from_external_source(__func__, __FILE__, fmt,    \
                                                 _level, __LINE__, 0 ,       \
                                                 ##args);                    \
                 }                                                           \
             }                                                               \
             break;                                                          \
         }                                                                   \
     } while (0)
 
 #define CRM_LOG_ASSERT(expr) do {                                       \
         if (!(expr)) {                                                  \
             static struct qb_log_callsite *core_cs = NULL;              \
             if(core_cs == NULL) {                                       \
                 core_cs = qb_log_callsite_get(__func__, __FILE__,       \
                                               "log-assert", LOG_TRACE,  \
                                               __LINE__, 0);             \
             }                                                           \
             crm_abort(__FILE__, __func__, __LINE__, #expr,              \
                       core_cs?core_cs->targets:FALSE, TRUE);            \
         }                                                               \
     } while(0)
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 /* 'failure_action' MUST NOT be 'continue' as it will apply to the
  * macro's do-while loop
  */
 #define CRM_CHECK(expr, failure_action) do {                            \
         if (!(expr)) {                                                  \
             static struct qb_log_callsite *core_cs = NULL;              \
             if (core_cs == NULL) {                                      \
                 core_cs = qb_log_callsite_get(__func__, __FILE__,       \
                                               "check-assert",           \
                                               LOG_TRACE, __LINE__, 0);  \
             }                                                           \
             crm_abort(__FILE__, __func__, __LINE__, #expr,              \
                 (core_cs? core_cs->targets: FALSE), TRUE);              \
             failure_action;                                             \
         }                                                               \
     } while(0)
 
 /*!
  * \brief Log XML line-by-line in a formatted fashion
  *
  * \param[in] level  Priority at which to log the messages
  * \param[in] text   Prefix for each line
  * \param[in] xml    XML to log
  *
  * \note This does nothing when \p level is \p LOG_STDOUT.
  */
 #define do_crm_log_xml(level, text, xml) do {                           \
         uint8_t _level = pcmk__clip_log_level(level);                   \
         static struct qb_log_callsite *xml_cs = NULL;                   \
                                                                         \
         switch (_level) {                                               \
             case LOG_STDOUT:                                            \
             case LOG_NEVER:                                             \
                 break;                                                  \
             default:                                                    \
                 if (xml_cs == NULL) {                                   \
                     xml_cs = qb_log_callsite_get(__func__, __FILE__,    \
                                                  "xml-blob", _level,    \
                                                  __LINE__, 0);          \
                 }                                                       \
                 if (crm_is_callsite_active(xml_cs, _level, 0)) {        \
                     pcmk_log_xml_as(__FILE__, __func__, __LINE__, 0,    \
                                     _level, text, (xml));               \
                 }                                                       \
                 break;                                                  \
         }                                                               \
     } while(0)
 
 /*!
  * \brief Log a message as if it came from a different code location
  *
  * \param[in] level     Priority at which to log the message
  * \param[in] file      Source file name to use instead of __FILE__
  * \param[in] function  Source function name to use instead of __func__
  * \param[in] line      Source line number to use instead of __line__
  * \param[in] fmt       printf-style format string literal for message
  * \param[in] args      Any arguments needed by format string
  */
 #define do_crm_log_alias(level, file, function, line, fmt, args...) do {    \
         uint8_t _level = pcmk__clip_log_level(level);                       \
                                                                             \
         switch (_level) {                                                   \
             case LOG_STDOUT:                                                \
                 printf(fmt "\n" , ##args);                                  \
                 break;                                                      \
             case LOG_NEVER:                                                 \
                 break;                                                      \
             default:                                                        \
                 qb_log_from_external_source(function, file, fmt, _level,    \
                                             line, 0 , ##args);              \
                 break;                                                      \
         }                                                                   \
     } while (0)
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 /*!
  * \brief Send a system error message to both the log and stderr
  *
  * \param[in] level  Priority at which to log the message
  * \param[in] fmt    printf-style format string for message
  * \param[in] args   Any arguments needed by format string
  *
  * \deprecated One of the other logging functions should be used with
  *             pcmk_strerror() instead.
  * \note This is a macro, and \p level may be evaluated more than once.
  * \note Because crm_perror() adds the system error message and error number
  *       onto the end of fmt, that information will become extended information
  *       if CRM_XS is used inside fmt and will not show up in syslog.
  */
 #define crm_perror(level, fmt, args...) do {                                \
         uint8_t _level = pcmk__clip_log_level(level);                       \
                                                                             \
         switch (_level) {                                                   \
             case LOG_NEVER:                                                 \
                 break;                                                      \
             default: {                                                      \
                 const char *err = strerror(errno);                          \
                 if (_level <= crm_log_level) {                              \
                     fprintf(stderr, fmt ": %s (%d)\n" , ##args, err,        \
                             errno);                                         \
                 }                                                           \
                 /* Pass original level arg since do_crm_log() also declares \
                  * _level                                                   \
                  */                                                         \
                 do_crm_log((level), fmt ": %s (%d)" , ##args, err, errno);  \
             }                                                               \
             break;                                                          \
         }                                                                   \
     } while (0)
 
 /*!
  * \brief Log a message with a tag (for use with PCMK_trace_tags)
  *
  * \param[in] level  Priority at which to log the message
  * \param[in] tag    String to tag message with
  * \param[in] fmt    printf-style format string for message
  * \param[in] args   Any arguments needed by format string
  *
  * \note This does nothing when level is LOG_STDOUT.
  */
 #define crm_log_tag(level, tag, fmt, args...) do {                          \
         uint8_t _level = pcmk__clip_log_level(level);                       \
                                                                             \
         switch (_level) {                                                   \
             case LOG_STDOUT: case LOG_NEVER:                                \
                 break;                                                      \
             default: {                                                      \
                 static struct qb_log_callsite *trace_tag_cs = NULL;         \
                 int converted_tag = g_quark_try_string(tag);                \
                 if (trace_tag_cs == NULL) {                                 \
                     trace_tag_cs = qb_log_callsite_get(__func__, __FILE__,  \
                                                        fmt, _level,         \
                                                        __LINE__,            \
                                                        converted_tag);      \
                 }                                                           \
                 if (crm_is_callsite_active(trace_tag_cs, _level,            \
                                            converted_tag)) {                \
                     qb_log_from_external_source(__func__, __FILE__, fmt,    \
                                                 _level, __LINE__,           \
                                                 converted_tag , ##args);    \
                 }                                                           \
             }                                                               \
         }                                                                   \
     } while (0)
 
 #define crm_emerg(fmt, args...)   qb_log(LOG_EMERG,       fmt , ##args)
 #define crm_crit(fmt, args...)    qb_logt(LOG_CRIT,    0, fmt , ##args)
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 #define crm_err(fmt, args...)     qb_logt(LOG_ERR,     0, fmt , ##args)
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 #define crm_warn(fmt, args...)    qb_logt(LOG_WARNING, 0, fmt , ##args)
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 #define crm_notice(fmt, args...)  qb_logt(LOG_NOTICE,  0, fmt , ##args)
 
 #define crm_info(fmt, args...)    qb_logt(LOG_INFO,    0, fmt , ##args)
                                                 //
 // NOTE: sbd (as of at least 1.5.2) uses this
 #define crm_debug(fmt, args...)   do_crm_log_unlikely(LOG_DEBUG, fmt , ##args)
 
 #define crm_trace(fmt, args...)   do_crm_log_unlikely(LOG_TRACE, fmt , ##args)
 
 #define crm_log_xml_crit(xml, text)    do_crm_log_xml(LOG_CRIT,    text, xml)
 #define crm_log_xml_err(xml, text)     do_crm_log_xml(LOG_ERR,     text, xml)
 #define crm_log_xml_warn(xml, text)    do_crm_log_xml(LOG_WARNING, text, xml)
 #define crm_log_xml_notice(xml, text)  do_crm_log_xml(LOG_NOTICE,  text, xml)
 #define crm_log_xml_info(xml, text)    do_crm_log_xml(LOG_INFO,    text, xml)
 #define crm_log_xml_debug(xml, text)   do_crm_log_xml(LOG_DEBUG,   text, xml)
 #define crm_log_xml_trace(xml, text)   do_crm_log_xml(LOG_TRACE,   text, xml)
 
 #define crm_log_xml_explicit(xml, text)  do {                   \
         static struct qb_log_callsite *digest_cs = NULL;        \
         digest_cs = qb_log_callsite_get(                        \
             __func__, __FILE__, text, LOG_TRACE, __LINE__,      \
             crm_trace_nonlog);                                  \
         if (digest_cs && digest_cs->targets) {                  \
             do_crm_log_xml(LOG_TRACE,   text, xml);             \
         }                                                       \
     } while(0)
 
-#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
-#include <crm/common/logging_compat.h>
-#endif
-
 #ifdef __cplusplus
 }
 #endif
 
 #endif
diff --git a/include/crm/common/logging_compat.h b/include/crm/common/logging_compat.h
deleted file mode 100644
index f6dec36838..0000000000
--- a/include/crm/common/logging_compat.h
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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.
- */
-
-#ifndef PCMK__CRM_COMMON_LOGGING_COMPAT__H
-#define PCMK__CRM_COMMON_LOGGING_COMPAT__H
-
-#include <stdint.h>         // uint8_t
-#include <glib.h>
-#include <libxml/tree.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/**
- * \file
- * \brief Deprecated Pacemaker logging API
- * \ingroup core
- * \deprecated Do not include this header directly. Do not use Pacemaker
- *             libraries for general-purpose logging; libqb's logging API is a
- *             suitable replacement. The logging APIs in this header, and the
- *             header itself, will be removed in a future release.
- */
-
-//! \deprecated This enum will be removed in a future release
-enum xml_log_options {
-    xml_log_option_filtered     = 0x0001,
-    xml_log_option_formatted    = 0x0002,
-    xml_log_option_text         = 0x0004,
-    xml_log_option_full_fledged = 0x0008,
-    xml_log_option_diff_plus    = 0x0010,
-    xml_log_option_diff_minus   = 0x0020,
-    xml_log_option_diff_short   = 0x0040,
-    xml_log_option_diff_all     = 0x0100,
-    xml_log_option_dirty_add    = 0x1000,
-    xml_log_option_open         = 0x2000,
-    xml_log_option_children     = 0x4000,
-    xml_log_option_close        = 0x8000,
-};
-
-/*!
- * \brief Log a message using constant priority
- *
- * \param[in] level     Priority at which to log the message
- * \param[in] fmt       printf-style format string literal for message
- * \param[in] args      Any arguments needed by format string
- *
- * \deprecated Do not use Pacemaker for general-purpose logging
- * \note This is a macro, and \p level may be evaluated more than once.
- *       This does nothing when level is LOG_STDOUT.
- */
-#define do_crm_log_always(level, fmt, args...) do {                         \
-        switch (level) {                                                    \
-            case LOG_STDOUT: case LOG_NEVER:                                \
-                break;                                                      \
-            default:                                                        \
-                qb_log((level), fmt , ##args);                              \
-                break;                                                      \
-        }                                                                   \
-    } while (0)
-
-//! \deprecated Do not use Pacemaker for general-purpose string handling
-#define crm_str(x) (const char *) ((x)? (x) : "<null>")
-
-//! \deprecated Do not use Pacemaker for general-purpose logging
-gboolean crm_log_cli_init(const char *entity);
-
-//! \deprecated Do not use Pacemaker for general-purpose logging
-gboolean crm_add_logfile(const char *filename);
-
-//! \deprecated Do not use Pacemaker for general-purpose logging
-void log_data_element(int log_level, const char *file, const char *function,
-                      int line, const char *prefix, const xmlNode *data,
-                      int depth, int legacy_options);
-
-//! \deprecated Do not use Pacemaker for general-purpose logging
-void pcmk_log_xml_impl(uint8_t level, const char *text, const xmlNode *xml);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif // PCMK__CRM_COMMON_LOGGING_COMPAT__H
diff --git a/include/crm/common/xml_compat.h b/include/crm/common/xml_compat.h
index 12ca631afe..54423eb3f6 100644
--- a/include/crm/common/xml_compat.h
+++ b/include/crm/common/xml_compat.h
@@ -1,216 +1,69 @@
 /*
  * 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 <glib.h>               // gboolean
 #include <libxml/tree.h>        // xmlNode
 
 #include <crm/common/nvpair.h>  // crm_xml_add()
 #include <crm/common/xml_names.h>   // PCMK_XE_CLONE
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Deprecated Pacemaker XML API
  * \ingroup core
  * \deprecated Do not include this header directly. The XML APIs in this
  *             header, and the header itself, will be removed in a future
  *             release.
  */
 
-//! \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"));
-}
-
 // NOTE: sbd (as of at least 1.5.2) uses this
 //! \deprecated Use name member directly
 static inline const char *
 crm_element_name(const xmlNode *xml)
 {
     return (xml == NULL)? NULL : (const char *) xml->name;
 }
 
-//! \deprecated Do not use
-char *crm_xml_escape(const char *text);
-
 // NOTE: sbd (as of at least 1.5.2) uses this
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *copy_xml(xmlNode *src_node);
 
-//! \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 *create_xml_node(xmlNode *parent, const char *name);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-xmlNode *pcmk_create_xml_text_node(xmlNode *parent, const char *name,
-                                   const char *content);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-xmlNode *pcmk_create_html_node(xmlNode *parent, const char *element_name,
-                               const char *id, const char *class_name,
-                               const char *text);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-xmlNode *first_named_child(const xmlNode *parent, const char *name);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-xmlNode *find_xml_node(const xmlNode *root, const char *search_path,
-                       gboolean must_find);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-xmlNode *crm_next_same_xml(const xmlNode *sibling);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-void xml_remove_prop(xmlNode *obj, const char *name);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-gboolean replace_xml_child(xmlNode *parent, xmlNode *child, xmlNode *update,
-                           gboolean delete_only);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-gboolean update_xml_child(xmlNode *child, xmlNode *to_update);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-int find_xml_children(xmlNode **children, xmlNode *root, const char *tag,
-                      const char *field, const char *value,
-                      gboolean search_matches);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-xmlNode *get_xpath_object_relative(const char *xpath, xmlNode *xml_obj,
-                                   int error_level);
-
-//! \deprecated Do not use
-void fix_plus_plus_recursive(xmlNode *target);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-gboolean add_message_xml(xmlNode *msg, const char *field, xmlNode *xml);
-
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-xmlNode *get_message_xml(const xmlNode *msg, const char *field);
-
-//! \deprecated Do not use
-const char *xml_latest_schema(void);
-
-//! \deprecated Do not use
-const char *get_schema_name(int version);
-
-//! \deprecated Do not use
-int get_schema_version(const char *name);
-
-//! \deprecated Do not use
-int update_validation(xmlNode **xml_blob, int *best, int max,
-                      gboolean transform, gboolean to_logs);
-
-//! \deprecated Do not use
-gboolean validate_xml(xmlNode *xml_blob, const char *validation,
-                      gboolean to_logs);
-
-//! \deprecated Do not use
-gboolean validate_xml_verbose(const xmlNode *xml_blob);
-
 // NOTE: sbd (as of at least 1.5.2) uses this
 //! \deprecated Do not use
 gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs);
 
-//! \deprecated Do not use
-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;
-    }
-}
-
-//! \deprecated Do not use
-void copy_in_properties(xmlNode *target, const xmlNode *src);
-
-//! \deprecated Do not use
-void expand_plus_plus(xmlNode * target, const char *name, const char *value);
-
 // NOTE: sbd (as of at least 1.5.2) uses this
 //! \deprecated Call \c crm_log_init() or \c crm_log_cli_init() instead
 void crm_xml_init(void);
 
 //! \deprecated Exit with \c crm_exit() instead
 void crm_xml_cleanup(void);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 void pcmk_free_xml_subtree(xmlNode *xml);
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 void free_xml(xmlNode *child);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *expand_idref(xmlNode *input, xmlNode *top);
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_XML_COMPAT__H
diff --git a/lib/common/logging.c b/lib/common/logging.c
index 0555d527d5..444f49365e 100644
--- a/lib/common/logging.c
+++ b/lib/common/logging.c
@@ -1,1297 +1,1270 @@
 /*
  * 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 <crm_internal.h>
 
 #include <sys/param.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <sys/stat.h>
 #include <sys/utsname.h>
 
 #include <stdio.h>
 #include <unistd.h>
 #include <string.h>
 #include <stdlib.h>
 #include <limits.h>
 #include <ctype.h>
 #include <pwd.h>
 #include <grp.h>
 #include <time.h>
 #include <libgen.h>
 #include <signal.h>
 #include <bzlib.h>
 
 #include <qb/qbdefs.h>
 
 #include <crm/crm.h>
 #include <crm/common/mainloop.h>
 
 // Use high-resolution (millisecond) timestamps if libqb supports them
 #ifdef QB_FEATURE_LOG_HIRES_TIMESTAMPS
 #define TIMESTAMP_FORMAT_SPEC "%%T"
 typedef struct timespec *log_time_t;
 #else
 #define TIMESTAMP_FORMAT_SPEC "%%t"
 typedef time_t log_time_t;
 #endif
 
 unsigned int crm_log_level = LOG_INFO;
 unsigned int crm_trace_nonlog = 0;
 bool pcmk__is_daemon = false;
 char *pcmk__our_nodename = NULL;
 
 static unsigned int crm_log_priority = LOG_NOTICE;
 static GLogFunc glib_log_default = NULL;
 static pcmk__output_t *logger_out = NULL;
 
 pcmk__config_error_func pcmk__config_error_handler = NULL;
 pcmk__config_warning_func pcmk__config_warning_handler = NULL;
 void *pcmk__config_error_context = NULL;
 void *pcmk__config_warning_context = NULL;
 
 static gboolean crm_tracing_enabled(void);
 
 static void
 crm_glib_handler(const gchar * log_domain, GLogLevelFlags flags, const gchar * message,
                  gpointer user_data)
 {
     int log_level = LOG_WARNING;
     GLogLevelFlags msg_level = (flags & G_LOG_LEVEL_MASK);
     static struct qb_log_callsite *glib_cs = NULL;
 
     if (glib_cs == NULL) {
         glib_cs = qb_log_callsite_get(__func__, __FILE__, "glib-handler",
                                       LOG_DEBUG, __LINE__, crm_trace_nonlog);
     }
 
     switch (msg_level) {
         case G_LOG_LEVEL_CRITICAL:
             log_level = LOG_CRIT;
 
             if (!crm_is_callsite_active(glib_cs, LOG_DEBUG, crm_trace_nonlog)) {
                 /* log and record how we got here */
                 crm_abort(__FILE__, __func__, __LINE__, message, TRUE, TRUE);
             }
             break;
 
         case G_LOG_LEVEL_ERROR:
             log_level = LOG_ERR;
             break;
         case G_LOG_LEVEL_MESSAGE:
             log_level = LOG_NOTICE;
             break;
         case G_LOG_LEVEL_INFO:
             log_level = LOG_INFO;
             break;
         case G_LOG_LEVEL_DEBUG:
             log_level = LOG_DEBUG;
             break;
 
         case G_LOG_LEVEL_WARNING:
         case G_LOG_FLAG_RECURSION:
         case G_LOG_FLAG_FATAL:
         case G_LOG_LEVEL_MASK:
             log_level = LOG_WARNING;
             break;
     }
 
     do_crm_log(log_level, "%s: %s", log_domain, message);
 }
 
 #ifndef NAME_MAX
 #  define NAME_MAX 256
 #endif
 
 /*!
  * \internal
  * \brief Write out a blackbox (enabling blackboxes if needed)
  *
  * \param[in] nsig  Signal number that was received
  *
  * \note This is a true signal handler, and so must be async-safe.
  */
 static void
 crm_trigger_blackbox(int nsig)
 {
     if(nsig == SIGTRAP) {
         /* Turn it on if it wasn't already */
         crm_enable_blackbox(nsig);
     }
     crm_write_blackbox(nsig, NULL);
 }
 
 void
 crm_log_deinit(void)
 {
     if (glib_log_default != NULL) {
         g_log_set_default_handler(glib_log_default, NULL);
     }
 }
 
 #define FMT_MAX 256
 
 /*!
  * \internal
  * \brief Set the log format string based on the passed-in method
  *
  * \param[in] method        The detail level of the log output
  * \param[in] daemon        The daemon ID included in error messages
  * \param[in] use_pid       Cached result of getpid() call, for efficiency
  * \param[in] use_nodename  Cached result of uname() call, for efficiency
  *
  */
 
 /* XXX __attribute__((nonnull)) for use_nodename parameter */
 static void
 set_format_string(int method, const char *daemon, pid_t use_pid,
                   const char *use_nodename)
 {
     if (method == QB_LOG_SYSLOG) {
         // The system log gets a simplified, user-friendly format
         crm_extended_logging(method, QB_FALSE);
         qb_log_format_set(method, "%g %p: %b");
 
     } else {
         // Everything else gets more detail, for advanced troubleshooting
 
         int offset = 0;
         char fmt[FMT_MAX];
 
         if (method > QB_LOG_STDERR) {
             // If logging to file, prefix with timestamp, node name, daemon ID
             offset += snprintf(fmt + offset, FMT_MAX - offset,
                                TIMESTAMP_FORMAT_SPEC " %s %-20s[%lu] ",
                                 use_nodename, daemon, (unsigned long) use_pid);
         }
 
         // Add function name (in parentheses)
         offset += snprintf(fmt + offset, FMT_MAX - offset, "(%%n");
         if (crm_tracing_enabled()) {
             // When tracing, add file and line number
             offset += snprintf(fmt + offset, FMT_MAX - offset, "@%%f:%%l");
         }
         offset += snprintf(fmt + offset, FMT_MAX - offset, ")");
 
         // Add tag (if any), severity, and actual message
         offset += snprintf(fmt + offset, FMT_MAX - offset, " %%g\t%%p: %%b");
 
         CRM_LOG_ASSERT(offset > 0);
         qb_log_format_set(method, fmt);
     }
 }
 
 #define DEFAULT_LOG_FILE CRM_LOG_DIR "/pacemaker.log"
 
 static bool
 logfile_disabled(const char *filename)
 {
     return pcmk__str_eq(filename, PCMK_VALUE_NONE, pcmk__str_casei)
            || pcmk__str_eq(filename, "/dev/null", pcmk__str_none);
 }
 
 /*!
  * \internal
  * \brief Fix log file ownership if group is wrong or doesn't have access
  *
  * \param[in] filename  Log file name (for logging only)
  * \param[in] logfd     Log file descriptor
  *
  * \return Standard Pacemaker return code
  */
 static int
 chown_logfile(const char *filename, int logfd)
 {
     uid_t pcmk_uid = 0;
     gid_t pcmk_gid = 0;
     struct stat st;
     int rc;
 
     // Get the log file's current ownership and permissions
     if (fstat(logfd, &st) < 0) {
         return errno;
     }
 
     // Any other errors don't prevent file from being used as log
 
     rc = pcmk_daemon_user(&pcmk_uid, &pcmk_gid);
     if (rc != pcmk_ok) {
         rc = pcmk_legacy2rc(rc);
         crm_warn("Not changing '%s' ownership because user information "
                  "unavailable: %s", filename, pcmk_rc_str(rc));
         return pcmk_rc_ok;
     }
     if ((st.st_gid == pcmk_gid)
         && ((st.st_mode & S_IRWXG) == (S_IRGRP|S_IWGRP))) {
         return pcmk_rc_ok;
     }
     if (fchown(logfd, pcmk_uid, pcmk_gid) < 0) {
         crm_warn("Couldn't change '%s' ownership to user %s gid %d: %s",
              filename, CRM_DAEMON_USER, pcmk_gid, strerror(errno));
     }
     return pcmk_rc_ok;
 }
 
 // Reset log file permissions (using environment variable if set)
 static void
 chmod_logfile(const char *filename, int logfd)
 {
     const char *modestr = pcmk__env_option(PCMK__ENV_LOGFILE_MODE);
     mode_t filemode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
 
     if (modestr != NULL) {
         long filemode_l = strtol(modestr, NULL, 8);
 
         if ((filemode_l != LONG_MIN) && (filemode_l != LONG_MAX)) {
             filemode = (mode_t) filemode_l;
         }
     }
     if ((filemode != 0) && (fchmod(logfd, filemode) < 0)) {
         crm_warn("Couldn't change '%s' mode to %04o: %s",
                  filename, filemode, strerror(errno));
     }
 }
 
 // If we're root, correct a log file's permissions if needed
 static int
 set_logfile_permissions(const char *filename, FILE *logfile)
 {
     if (geteuid() == 0) {
         int logfd = fileno(logfile);
         int rc = chown_logfile(filename, logfd);
 
         if (rc != pcmk_rc_ok) {
             return rc;
         }
         chmod_logfile(filename, logfd);
     }
     return pcmk_rc_ok;
 }
 
 // Enable libqb logging to a new log file
 static void
 enable_logfile(int fd)
 {
     qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_TRUE);
 #if 0
     qb_log_ctl(fd, QB_LOG_CONF_FILE_SYNC, 1); // Turn on synchronous writes
 #endif
 
 #ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN
     // Longer than default, for logging long XML lines
     qb_log_ctl(fd, QB_LOG_CONF_MAX_LINE_LEN, 800);
 #endif
 
     crm_update_callsites();
 }
 
 static inline void
 disable_logfile(int fd)
 {
     qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_FALSE);
 }
 
 static void
 setenv_logfile(const char *filename)
 {
     // Some resource agents will log only if environment variable is set
     if (pcmk__env_option(PCMK__ENV_LOGFILE) == NULL) {
         pcmk__set_env_option(PCMK__ENV_LOGFILE, filename, true);
     }
 }
 
 /*!
  * \brief Add a file to be used as a Pacemaker detail log
  *
  * \param[in] filename  Name of log file to use
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__add_logfile(const char *filename)
 {
     /* No log messages from this function will be logged to the new log!
      * If another target such as syslog has already been added, the messages
      * should show up there.
      */
 
     int fd = 0;
     int rc = pcmk_rc_ok;
     FILE *logfile = NULL;
     bool is_default = false;
 
     static int default_fd = -1;
     static bool have_logfile = false;
 
     // Use default if caller didn't specify (and we don't already have one)
     if (filename == NULL) {
         if (have_logfile) {
             return pcmk_rc_ok;
         }
         filename = DEFAULT_LOG_FILE;
     }
 
     // If the user doesn't want logging, we're done
     if (logfile_disabled(filename)) {
         return pcmk_rc_ok;
     }
 
     // If the caller wants the default and we already have it, we're done
     is_default = pcmk__str_eq(filename, DEFAULT_LOG_FILE, pcmk__str_none);
     if (is_default && (default_fd >= 0)) {
         return pcmk_rc_ok;
     }
 
     // Check whether we have write access to the file
     logfile = fopen(filename, "a");
     if (logfile == NULL) {
         rc = errno;
         crm_warn("Logging to '%s' is disabled: %s " CRM_XS " uid=%u gid=%u",
                  filename, strerror(rc), geteuid(), getegid());
         return rc;
     }
 
     rc = set_logfile_permissions(filename, logfile);
     if (rc != pcmk_rc_ok) {
         crm_warn("Logging to '%s' is disabled: %s " CRM_XS " permissions",
                  filename, strerror(rc));
         fclose(logfile);
         return rc;
     }
 
     // Close and reopen as libqb logging target
     fclose(logfile);
     fd = qb_log_file_open(filename);
     if (fd < 0) {
         crm_warn("Logging to '%s' is disabled: %s " CRM_XS " qb_log_file_open",
                  filename, strerror(-fd));
         return -fd; // == +errno
     }
 
     if (is_default) {
         default_fd = fd;
         setenv_logfile(filename);
 
     } else if (default_fd >= 0) {
         crm_notice("Switching logging to %s", filename);
         disable_logfile(default_fd);
     }
 
     crm_notice("Additional logging available in %s", filename);
     enable_logfile(fd);
     have_logfile = true;
     return pcmk_rc_ok;
 }
 
 /*!
  * \brief Add multiple additional log files
  *
  * \param[in] log_files  Array of log files to add
  * \param[in] out        Output object to use for error reporting
  *
  * \return Standard Pacemaker return code
  */
 void
 pcmk__add_logfiles(gchar **log_files, pcmk__output_t *out)
 {
     if (log_files == NULL) {
         return;
     }
 
     for (gchar **fname = log_files; *fname != NULL; fname++) {
         int rc = pcmk__add_logfile(*fname);
 
         if (rc != pcmk_rc_ok) {
             out->err(out, "Logging to %s is disabled: %s",
                      *fname, pcmk_rc_str(rc));
         }
     }
 }
 
 static int blackbox_trigger = 0;
 static volatile char *blackbox_file_prefix = NULL;
 
 static void
 blackbox_logger(int32_t t, struct qb_log_callsite *cs, log_time_t timestamp,
                 const char *msg)
 {
     if(cs && cs->priority < LOG_ERR) {
         crm_write_blackbox(SIGTRAP, cs); /* Bypass the over-dumping logic */
     } else {
         crm_write_blackbox(0, cs);
     }
 }
 
 static void
 crm_control_blackbox(int nsig, bool enable)
 {
     int lpc = 0;
 
     if (blackbox_file_prefix == NULL) {
         pid_t pid = getpid();
 
         blackbox_file_prefix = crm_strdup_printf("%s/%s-%lu",
                                                  CRM_BLACKBOX_DIR,
                                                  crm_system_name,
                                                  (unsigned long) pid);
     }
 
     if (enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) {
         qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_SIZE, 5 * 1024 * 1024); /* Any size change drops existing entries */
         qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE);      /* Setting the size seems to disable it */
 
         /* Enable synchronous logging */
         for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) {
             qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_TRUE);
         }
 
         crm_notice("Initiated blackbox recorder: %s", blackbox_file_prefix);
 
         /* Save to disk on abnormal termination */
         crm_signal_handler(SIGSEGV, crm_trigger_blackbox);
         crm_signal_handler(SIGABRT, crm_trigger_blackbox);
         crm_signal_handler(SIGILL,  crm_trigger_blackbox);
         crm_signal_handler(SIGBUS,  crm_trigger_blackbox);
         crm_signal_handler(SIGFPE,  crm_trigger_blackbox);
 
         crm_update_callsites();
 
         blackbox_trigger = qb_log_custom_open(blackbox_logger, NULL, NULL, NULL);
         qb_log_ctl(blackbox_trigger, QB_LOG_CONF_ENABLED, QB_TRUE);
         crm_trace("Trigger: %d is %d %d", blackbox_trigger,
                   qb_log_ctl(blackbox_trigger, QB_LOG_CONF_STATE_GET, 0), QB_LOG_STATE_ENABLED);
 
         crm_update_callsites();
 
     } else if (!enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) == QB_LOG_STATE_ENABLED) {
         qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE);
 
         /* Disable synchronous logging again when the blackbox is disabled */
         for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) {
             qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_FALSE);
         }
     }
 }
 
 void
 crm_enable_blackbox(int nsig)
 {
     crm_control_blackbox(nsig, TRUE);
 }
 
 void
 crm_disable_blackbox(int nsig)
 {
     crm_control_blackbox(nsig, FALSE);
 }
 
 /*!
  * \internal
  * \brief Write out a blackbox, if blackboxes are enabled
  *
  * \param[in] nsig  Signal that was received
  * \param[in] cs    libqb callsite
  *
  * \note This may be called via a true signal handler and so must be async-safe.
  * @TODO actually make this async-safe
  */
 void
 crm_write_blackbox(int nsig, const struct qb_log_callsite *cs)
 {
     static volatile int counter = 1;
     static volatile time_t last = 0;
 
     char buffer[NAME_MAX];
     time_t now = time(NULL);
 
     if (blackbox_file_prefix == NULL) {
         return;
     }
 
     switch (nsig) {
         case 0:
         case SIGTRAP:
             /* The graceful case - such as assertion failure or user request */
 
             if (nsig == 0 && now == last) {
                 /* Prevent over-dumping */
                 return;
             }
 
             snprintf(buffer, NAME_MAX, "%s.%d", blackbox_file_prefix, counter++);
             if (nsig == SIGTRAP) {
                 crm_notice("Blackbox dump requested, please see %s for contents", buffer);
 
             } else if (cs) {
                 syslog(LOG_NOTICE,
                        "Problem detected at %s:%d (%s), please see %s for additional details",
                        cs->function, cs->lineno, cs->filename, buffer);
             } else {
                 crm_notice("Problem detected, please see %s for additional details", buffer);
             }
 
             last = now;
             qb_log_blackbox_write_to_file(buffer);
 
             /* Flush the existing contents
              * A size change would also work
              */
             qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE);
             qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE);
             break;
 
         default:
             /* Do as little as possible, just try to get what we have out
              * We logged the filename when the blackbox was enabled
              */
             crm_signal_handler(nsig, SIG_DFL);
             qb_log_blackbox_write_to_file((const char *)blackbox_file_prefix);
             qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE);
             raise(nsig);
             break;
     }
 }
 
 static const char *
 crm_quark_to_string(uint32_t tag)
 {
     const char *text = g_quark_to_string(tag);
 
     if (text) {
         return text;
     }
     return "";
 }
 
 static void
 crm_log_filter_source(int source, const char *trace_files, const char *trace_fns,
                       const char *trace_fmts, const char *trace_tags, const char *trace_blackbox,
                       struct qb_log_callsite *cs)
 {
     if (qb_log_ctl(source, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) {
         return;
     } else if (cs->tags != crm_trace_nonlog && source == QB_LOG_BLACKBOX) {
         /* Blackbox gets everything if enabled */
         qb_bit_set(cs->targets, source);
 
     } else if (source == blackbox_trigger && blackbox_trigger > 0) {
         /* Should this log message result in the blackbox being dumped */
         if (cs->priority <= LOG_ERR) {
             qb_bit_set(cs->targets, source);
 
         } else if (trace_blackbox) {
             char *key = crm_strdup_printf("%s:%d", cs->function, cs->lineno);
 
             if (strstr(trace_blackbox, key) != NULL) {
                 qb_bit_set(cs->targets, source);
             }
             free(key);
         }
 
     } else if (source == QB_LOG_SYSLOG) {       /* No tracing to syslog */
         if (cs->priority <= crm_log_priority && cs->priority <= crm_log_level) {
             qb_bit_set(cs->targets, source);
         }
         /* Log file tracing options... */
     } else if (cs->priority <= crm_log_level) {
         qb_bit_set(cs->targets, source);
     } else if (trace_files && strstr(trace_files, cs->filename) != NULL) {
         qb_bit_set(cs->targets, source);
     } else if (trace_fns && strstr(trace_fns, cs->function) != NULL) {
         qb_bit_set(cs->targets, source);
     } else if (trace_fmts && strstr(trace_fmts, cs->format) != NULL) {
         qb_bit_set(cs->targets, source);
     } else if (trace_tags
                && cs->tags != 0
                && cs->tags != crm_trace_nonlog && g_quark_to_string(cs->tags) != NULL) {
         qb_bit_set(cs->targets, source);
     }
 }
 
 #ifndef HAVE_STRCHRNUL
 /* strchrnul() is a GNU extension. If not present, use our own definition.
  * The GNU version returns char*, but we only need it to be const char*.
  */
 static const char *
 strchrnul(const char *s, int c)
 {
     while ((*s != c) && (*s != '\0')) {
         ++s;
     }
     return s;
 }
 #endif
 
 static void
 crm_log_filter(struct qb_log_callsite *cs)
 {
     int lpc = 0;
     static int need_init = 1;
     static const char *trace_fns = NULL;
     static const char *trace_tags = NULL;
     static const char *trace_fmts = NULL;
     static const char *trace_files = NULL;
     static const char *trace_blackbox = NULL;
 
     if (need_init) {
         need_init = 0;
         trace_fns = pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS);
         trace_fmts = pcmk__env_option(PCMK__ENV_TRACE_FORMATS);
         trace_tags = pcmk__env_option(PCMK__ENV_TRACE_TAGS);
         trace_files = pcmk__env_option(PCMK__ENV_TRACE_FILES);
         trace_blackbox = pcmk__env_option(PCMK__ENV_TRACE_BLACKBOX);
 
         if (trace_tags != NULL) {
             uint32_t tag;
             char token[500];
             const char *offset = NULL;
             const char *next = trace_tags;
 
             do {
                 offset = next;
                 next = strchrnul(offset, ',');
                 snprintf(token, sizeof(token), "%.*s", (int)(next - offset), offset);
 
                 tag = g_quark_from_string(token);
                 crm_info("Created GQuark %u from token '%s' in '%s'", tag, token, trace_tags);
 
                 if (next[0] != 0) {
                     next++;
                 }
 
             } while (next != NULL && next[0] != 0);
         }
     }
 
     cs->targets = 0;            /* Reset then find targets to enable */
     for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) {
         crm_log_filter_source(lpc, trace_files, trace_fns, trace_fmts, trace_tags, trace_blackbox,
                               cs);
     }
 }
 
 gboolean
 crm_is_callsite_active(struct qb_log_callsite *cs, uint8_t level, uint32_t tags)
 {
     gboolean refilter = FALSE;
 
     if (cs == NULL) {
         return FALSE;
     }
 
     if (cs->priority != level) {
         cs->priority = level;
         refilter = TRUE;
     }
 
     if (cs->tags != tags) {
         cs->tags = tags;
         refilter = TRUE;
     }
 
     if (refilter) {
         crm_log_filter(cs);
     }
 
     if (cs->targets == 0) {
         return FALSE;
     }
     return TRUE;
 }
 
 void
 crm_update_callsites(void)
 {
     static gboolean log = TRUE;
 
     if (log) {
         log = FALSE;
         crm_debug
             ("Enabling callsites based on priority=%d, files=%s, functions=%s, formats=%s, tags=%s",
              crm_log_level, pcmk__env_option(PCMK__ENV_TRACE_FILES),
              pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS),
              pcmk__env_option(PCMK__ENV_TRACE_FORMATS),
              pcmk__env_option(PCMK__ENV_TRACE_TAGS));
     }
     qb_log_filter_fn_set(crm_log_filter);
 }
 
 static gboolean
 crm_tracing_enabled(void)
 {
     return (crm_log_level == LOG_TRACE)
             || (pcmk__env_option(PCMK__ENV_TRACE_FILES) != NULL)
             || (pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS) != NULL)
             || (pcmk__env_option(PCMK__ENV_TRACE_FORMATS) != NULL)
             || (pcmk__env_option(PCMK__ENV_TRACE_TAGS) != NULL);
 }
 
 static int
 crm_priority2int(const char *name)
 {
     struct syslog_names {
         const char *name;
         int priority;
     };
     static struct syslog_names p_names[] = {
         {"emerg", LOG_EMERG},
         {"alert", LOG_ALERT},
         {"crit", LOG_CRIT},
         {"error", LOG_ERR},
         {"warning", LOG_WARNING},
         {"notice", LOG_NOTICE},
         {"info", LOG_INFO},
         {"debug", LOG_DEBUG},
         {NULL, -1}
     };
     int lpc;
 
     for (lpc = 0; name != NULL && p_names[lpc].name != NULL; lpc++) {
         if (pcmk__str_eq(p_names[lpc].name, name, pcmk__str_none)) {
             return p_names[lpc].priority;
         }
     }
     return crm_log_priority;
 }
 
 
 /*!
  * \internal
  * \brief Set the identifier for the current process
  *
  * If the identifier crm_system_name is not already set, then it is set as follows:
  * - it is passed to the function via the "entity" parameter, or
  * - it is derived from the executable name
  *
  * The identifier can be used in logs, IPC, and more.
  *
  * This method also sets the PCMK_service environment variable.
  *
  * \param[in] entity  If not NULL, will be assigned to the identifier
  * \param[in] argc    The number of command line parameters
  * \param[in] argv    The command line parameter values
  */
 static void
 set_identity(const char *entity, int argc, char *const *argv)
 {
     if (crm_system_name != NULL) {
         return; // Already set, don't overwrite
     }
 
     if (entity != NULL) {
         crm_system_name = pcmk__str_copy(entity);
 
     } else if ((argc > 0) && (argv != NULL)) {
         char *mutable = strdup(argv[0]);
         char *modified = basename(mutable);
 
         if (strstr(modified, "lt-") == modified) {
             modified += 3;
         }
         crm_system_name = pcmk__str_copy(modified);
         free(mutable);
 
     } else {
         crm_system_name = pcmk__str_copy("Unknown");
     }
 
     // Used by fencing.py.py (in fence-agents)
     pcmk__set_env_option(PCMK__ENV_SERVICE, crm_system_name, false);
 }
 
 void
 crm_log_preinit(const char *entity, int argc, char *const *argv)
 {
     /* Configure libqb logging with nothing turned on */
 
     struct utsname res;
     int lpc = 0;
     int32_t qb_facility = 0;
     pid_t pid = getpid();
     const char *nodename = "localhost";
     static bool have_logging = false;
 
     if (have_logging) {
         return;
     }
 
     have_logging = true;
 
     pcmk__xml_init();
 
     if (crm_trace_nonlog == 0) {
         crm_trace_nonlog = g_quark_from_static_string("Pacemaker non-logging tracepoint");
     }
 
     umask(S_IWGRP | S_IWOTH | S_IROTH);
 
     /* Redirect messages from glib functions to our handler */
     glib_log_default = g_log_set_default_handler(crm_glib_handler, NULL);
 
     /* and for good measure... - this enum is a bit field (!) */
     g_log_set_always_fatal((GLogLevelFlags) 0); /*value out of range */
 
     /* Set crm_system_name, which is used as the logging name. It may also
      * be used for other purposes such as an IPC client name.
      */
     set_identity(entity, argc, argv);
 
     qb_facility = qb_log_facility2int("local0");
     qb_log_init(crm_system_name, qb_facility, LOG_ERR);
     crm_log_level = LOG_CRIT;
 
     /* Nuke any syslog activity until it's asked for */
     qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_FALSE);
 #ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN
     // Shorter than default, generous for what we *should* send to syslog
     qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_MAX_LINE_LEN, 256);
 #endif
     if (uname(memset(&res, 0, sizeof(res))) == 0 && *res.nodename != '\0') {
         nodename = res.nodename;
     }
 
     /* Set format strings and disable threading
      * Pacemaker and threads do not mix well (due to the amount of forking)
      */
     qb_log_tags_stringify_fn_set(crm_quark_to_string);
     for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) {
         qb_log_ctl(lpc, QB_LOG_CONF_THREADED, QB_FALSE);
 #ifdef HAVE_qb_log_conf_QB_LOG_CONF_ELLIPSIS
         // End truncated lines with '...'
         qb_log_ctl(lpc, QB_LOG_CONF_ELLIPSIS, QB_TRUE);
 #endif
         set_format_string(lpc, crm_system_name, pid, nodename);
     }
 
 #ifdef ENABLE_NLS
     /* Enable translations (experimental). Currently we only have a few
      * proof-of-concept translations for some option help. The goal would be to
      * offer translations for option help and man pages rather than logs or
      * documentation, to reduce the burden of maintaining them.
      */
 
     // Load locale information for the local host from the environment
     setlocale(LC_ALL, "");
 
     // Tell gettext where to find Pacemaker message catalogs
     CRM_ASSERT(bindtextdomain(PACKAGE, PCMK__LOCALE_DIR) != NULL);
 
     // Tell gettext to use the Pacemaker message catalogs
     CRM_ASSERT(textdomain(PACKAGE) != NULL);
 
     // Tell gettext that the translated strings are stored in UTF-8
     bind_textdomain_codeset(PACKAGE, "UTF-8");
 #endif
 }
 
 gboolean
 crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_stderr,
              int argc, char **argv, gboolean quiet)
 {
     const char *syslog_priority = NULL;
     const char *facility = pcmk__env_option(PCMK__ENV_LOGFACILITY);
     const char *f_copy = facility;
 
     pcmk__is_daemon = daemon;
     crm_log_preinit(entity, argc, argv);
 
     if (level > LOG_TRACE) {
         level = LOG_TRACE;
     }
     if(level > crm_log_level) {
         crm_log_level = level;
     }
 
     /* Should we log to syslog */
     if (facility == NULL) {
         if (pcmk__is_daemon) {
             facility = "daemon";
         } else {
             facility = PCMK_VALUE_NONE;
         }
         pcmk__set_env_option(PCMK__ENV_LOGFACILITY, facility, true);
     }
 
     if (pcmk__str_eq(facility, PCMK_VALUE_NONE, pcmk__str_casei)) {
         quiet = TRUE;
 
 
     } else {
         qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_FACILITY, qb_log_facility2int(facility));
     }
 
     if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_DEBUG)) {
         /* Override the default setting */
         crm_log_level = LOG_DEBUG;
     }
 
     /* What lower threshold do we have for sending to syslog */
     syslog_priority = pcmk__env_option(PCMK__ENV_LOGPRIORITY);
     if (syslog_priority) {
         crm_log_priority = crm_priority2int(syslog_priority);
     }
     qb_log_filter_ctl(QB_LOG_SYSLOG, QB_LOG_FILTER_ADD, QB_LOG_FILTER_FILE, "*",
                       crm_log_priority);
 
     // Log to syslog unless requested to be quiet
     if (!quiet) {
         qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_TRUE);
     }
 
     /* Should we log to stderr */ 
     if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_STDERR)) {
         /* Override the default setting */
         to_stderr = TRUE;
     }
     crm_enable_stderr(to_stderr);
 
     // Log to a file if we're a daemon or user asked for one
     {
         const char *logfile = pcmk__env_option(PCMK__ENV_LOGFILE);
 
         if (!pcmk__str_eq(PCMK_VALUE_NONE, logfile, pcmk__str_casei)
             && (pcmk__is_daemon || (logfile != NULL))) {
             // Daemons always get a log file, unless explicitly set to "none"
             pcmk__add_logfile(logfile);
         }
     }
 
     if (pcmk__is_daemon
         && pcmk__env_option_enabled(crm_system_name, PCMK__ENV_BLACKBOX)) {
         crm_enable_blackbox(0);
     }
 
     /* Summary */
     crm_trace("Quiet: %d, facility %s", quiet, f_copy);
     pcmk__env_option(PCMK__ENV_LOGFILE);
     pcmk__env_option(PCMK__ENV_LOGFACILITY);
 
     crm_update_callsites();
 
     /* Ok, now we can start logging... */
 
     // Disable daemon request if user isn't root or Pacemaker daemon user
     if (pcmk__is_daemon) {
         const char *user = getenv("USER");
 
         if (user != NULL && !pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) {
             crm_trace("Not switching to corefile directory for %s", user);
             pcmk__is_daemon = false;
         }
     }
 
     if (pcmk__is_daemon) {
         int user = getuid();
         struct passwd *pwent = getpwuid(user);
 
         if (pwent == NULL) {
             crm_perror(LOG_ERR, "Cannot get name for uid: %d", user);
 
         } else if (!pcmk__strcase_any_of(pwent->pw_name, "root", CRM_DAEMON_USER, NULL)) {
             crm_trace("Don't change active directory for regular user: %s", pwent->pw_name);
 
         } else if (chdir(CRM_CORE_DIR) < 0) {
             crm_perror(LOG_INFO, "Cannot change active directory to " CRM_CORE_DIR);
 
         } else {
             crm_info("Changed active directory to " CRM_CORE_DIR);
         }
 
         /* Original meanings from signal(7)
          *
          * Signal       Value     Action   Comment
          * SIGTRAP        5        Core    Trace/breakpoint trap
          * SIGUSR1     30,10,16    Term    User-defined signal 1
          * SIGUSR2     31,12,17    Term    User-defined signal 2
          *
          * Our usage is as similar as possible
          */
         mainloop_add_signal(SIGUSR1, crm_enable_blackbox);
         mainloop_add_signal(SIGUSR2, crm_disable_blackbox);
         mainloop_add_signal(SIGTRAP, crm_trigger_blackbox);
 
     } else if (!quiet) {
         crm_log_args(argc, argv);
     }
 
     return TRUE;
 }
 
 /* returns the old value */
 unsigned int
 set_crm_log_level(unsigned int level)
 {
     unsigned int old = crm_log_level;
 
     if (level > LOG_TRACE) {
         level = LOG_TRACE;
     }
     crm_log_level = level;
     crm_update_callsites();
     crm_trace("New log level: %d", level);
     return old;
 }
 
 void
 crm_enable_stderr(int enable)
 {
     if (enable && qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) {
         qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_TRUE);
         crm_update_callsites();
 
     } else if (enable == FALSE) {
         qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_FALSE);
     }
 }
 
 /*!
  * \brief Make logging more verbose
  *
  * If logging to stderr is not already enabled when this function is called,
  * enable it. Otherwise, increase the log level by 1.
  *
  * \param[in] argc  Ignored
  * \param[in] argv  Ignored
  */
 void
 crm_bump_log_level(int argc, char **argv)
 {
     if (qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0)
         != QB_LOG_STATE_ENABLED) {
         crm_enable_stderr(TRUE);
     } else {
         set_crm_log_level(crm_log_level + 1);
     }
 }
 
 unsigned int
 get_crm_log_level(void)
 {
     return crm_log_level;
 }
 
 /*!
  * \brief Log the command line (once)
  *
  * \param[in]  Number of values in \p argv
  * \param[in]  Command-line arguments (including command name)
  *
  * \note This function will only log once, even if called with different
  *       arguments.
  */
 void
 crm_log_args(int argc, char **argv)
 {
     static bool logged = false;
     gchar *arg_string = NULL;
 
     if ((argc == 0) || (argv == NULL) || logged) {
         return;
     }
     logged = true;
     arg_string = g_strjoinv(" ", argv);
     crm_notice("Invoked: %s", arg_string);
     g_free(arg_string);
 }
 
 void
 crm_log_output_fn(const char *file, const char *function, int line, int level, const char *prefix,
                   const char *output)
 {
     const char *next = NULL;
     const char *offset = NULL;
 
     if (level == LOG_NEVER) {
         return;
     }
 
     if (output == NULL) {
         if (level != LOG_STDOUT) {
             level = LOG_TRACE;
         }
         output = "-- empty --";
     }
 
     next = output;
     do {
         offset = next;
         next = strchrnul(offset, '\n');
         do_crm_log_alias(level, file, function, line, "%s [ %.*s ]", prefix,
                          (int)(next - offset), offset);
         if (next[0] != 0) {
             next++;
         }
 
     } while (next != NULL && next[0] != 0);
 }
 
 void
 pcmk__cli_init_logging(const char *name, unsigned int verbosity)
 {
     crm_log_init(name, LOG_ERR, FALSE, FALSE, 0, NULL, TRUE);
 
     for (int i = 0; i < verbosity; i++) {
         /* These arguments are ignored, so pass placeholders. */
         crm_bump_log_level(0, NULL);
     }
 }
 
 /*!
  * \brief Log XML line-by-line in a formatted fashion
  *
  * \param[in] file      File name to use for log filtering
  * \param[in] function  Function name to use for log filtering
  * \param[in] line      Line number to use for log filtering
  * \param[in] tags      Logging tags to use for log filtering
  * \param[in] level     Priority at which to log the messages
  * \param[in] text      Prefix for each line
  * \param[in] xml       XML to log
  *
  * \note This does nothing when \p level is \p LOG_STDOUT.
  * \note Do not call this function directly. It should be called only from the
  *       \p do_crm_log_xml() macro.
  */
 void
 pcmk_log_xml_as(const char *file, const char *function, uint32_t line,
                 uint32_t tags, uint8_t level, const char *text, const xmlNode *xml)
 {
     if (xml == NULL) {
         do_crm_log(level, "%s%sNo data to dump as XML",
                    pcmk__s(text, ""), pcmk__str_empty(text)? "" : " ");
 
     } else {
         if (logger_out == NULL) {
             CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return);
         }
 
         pcmk__output_set_log_level(logger_out, level);
         pcmk__output_set_log_filter(logger_out, file, function, line, tags);
         pcmk__xml_show(logger_out, text, xml, 1,
                        pcmk__xml_fmt_pretty
                        |pcmk__xml_fmt_open
                        |pcmk__xml_fmt_children
                        |pcmk__xml_fmt_close);
         pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U);
     }
 }
 
 /*!
  * \internal
  * \brief Log XML changes line-by-line in a formatted fashion
  *
  * \param[in] file      File name to use for log filtering
  * \param[in] function  Function name to use for log filtering
  * \param[in] line      Line number to use for log filtering
  * \param[in] tags      Logging tags to use for log filtering
  * \param[in] level     Priority at which to log the messages
  * \param[in] xml       XML whose changes to log
  *
  * \note This does nothing when \p level is \c LOG_STDOUT.
  */
 void
 pcmk__log_xml_changes_as(const char *file, const char *function, uint32_t line,
                          uint32_t tags, uint8_t level, const xmlNode *xml)
 {
     if (xml == NULL) {
         do_crm_log(level, "No XML to dump");
         return;
     }
 
     if (logger_out == NULL) {
         CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return);
     }
     pcmk__output_set_log_level(logger_out, level);
     pcmk__output_set_log_filter(logger_out, file, function, line, tags);
     pcmk__xml_show_changes(logger_out, xml);
     pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U);
 }
 
 /*!
  * \internal
  * \brief Log an XML patchset line-by-line in a formatted fashion
  *
  * \param[in] file      File name to use for log filtering
  * \param[in] function  Function name to use for log filtering
  * \param[in] line      Line number to use for log filtering
  * \param[in] tags      Logging tags to use for log filtering
  * \param[in] level     Priority at which to log the messages
  * \param[in] patchset  XML patchset to log
  *
  * \note This does nothing when \p level is \c LOG_STDOUT.
  */
 void
 pcmk__log_xml_patchset_as(const char *file, const char *function, uint32_t line,
                           uint32_t tags, uint8_t level, const xmlNode *patchset)
 {
     if (patchset == NULL) {
         do_crm_log(level, "No patchset to dump");
         return;
     }
 
     if (logger_out == NULL) {
         CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return);
     }
     pcmk__output_set_log_level(logger_out, level);
     pcmk__output_set_log_filter(logger_out, file, function, line, tags);
     logger_out->message(logger_out, "xml-patchset", patchset);
     pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U);
 }
 
 /*!
  * \internal
  * \brief Free the logging library's internal log output object
  */
 void
 pcmk__free_common_logger(void)
 {
     if (logger_out != NULL) {
         logger_out->finish(logger_out, CRM_EX_OK, true, NULL);
         pcmk__output_free(logger_out);
         logger_out = NULL;
     }
 }
 
-// Deprecated functions kept only for backward API compatibility
-// LCOV_EXCL_START
-
-#include <crm/common/logging_compat.h>
-
-gboolean
-crm_log_cli_init(const char *entity)
-{
-    pcmk__cli_init_logging(entity, 0);
-    return TRUE;
-}
-
-gboolean
-crm_add_logfile(const char *filename)
-{
-    return pcmk__add_logfile(filename) == pcmk_rc_ok;
-}
-
-void
-pcmk_log_xml_impl(uint8_t level, const char *text, const xmlNode *xml)
-{
-    pcmk_log_xml_as(__FILE__, __func__, __LINE__, 0, level, text, xml);
-}
-
-// LCOV_EXCL_STOP
-// End deprecated API
-
 void pcmk__set_config_error_handler(pcmk__config_error_func error_handler, void *error_context)
 {
     pcmk__config_error_handler = error_handler;
     pcmk__config_error_context = error_context;    
 }
 
 void pcmk__set_config_warning_handler(pcmk__config_warning_func warning_handler, void *warning_context)
 {
     pcmk__config_warning_handler = warning_handler;
     pcmk__config_warning_context = warning_context;   
 }
diff --git a/lib/common/messages.c b/lib/common/messages.c
index c619aacc0e..2e78f8bcde 100644
--- a/lib/common/messages.c
+++ b/lib/common/messages.c
@@ -1,309 +1,284 @@
 /*
  * 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 <crm_internal.h>
 
 #include <stdio.h>
 #include <sys/types.h>
 
 #include <glib.h>
 #include <libxml/tree.h>
 
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 
 /*!
  * \brief Create a Pacemaker request (for IPC or cluster layer)
  *
  * \param[in] task          What to set as the request's task
  * \param[in] msg_data      What to add as the request's data contents
  * \param[in] host_to       What to set as the request's destination host
  * \param[in] sys_to        What to set as the request's destination system
  * \param[in] sys_from      If not NULL, set as request's origin system
  * \param[in] uuid_from     If not NULL, use in request's origin system
  * \param[in] origin        Name of function that called this one
  *
  * \return XML of new request
  *
  * \note One of sys_from or uuid_from must be non-NULL
  * \note This function should not be called directly, but via the
  *       create_request() wrapper.
  * \note The caller is responsible for freeing the return value using
  *       \c pcmk__xml_free().
  */
 xmlNode *
 create_request_adv(const char *task, xmlNode *msg_data,
                    const char *host_to, const char *sys_to,
                    const char *sys_from, const char *uuid_from,
                    const char *origin)
 {
     static uint ref_counter = 0;
 
     char *true_from = NULL;
     xmlNode *request = NULL;
     char *reference = crm_strdup_printf("%s-%s-%lld-%u",
                                         (task? task : "_empty_"),
                                         (sys_from? sys_from : "_empty_"),
                                         (long long) time(NULL), ref_counter++);
 
     if (uuid_from != NULL) {
         true_from = crm_strdup_printf("%s_%s", uuid_from,
                                       (sys_from? sys_from : "none"));
     } else if (sys_from != NULL) {
         true_from = strdup(sys_from);
     } else {
         crm_err("Cannot create IPC request: No originating system specified");
     }
 
     // host_from will get set for us if necessary by the controller when routed
     request = pcmk__xe_create(NULL, __func__);
     crm_xml_add(request, PCMK_XA_ORIGIN, origin);
     crm_xml_add(request, PCMK__XA_T, PCMK__VALUE_CRMD);
     crm_xml_add(request, PCMK_XA_VERSION, CRM_FEATURE_SET);
     crm_xml_add(request, PCMK__XA_SUBT, PCMK__VALUE_REQUEST);
     crm_xml_add(request, PCMK_XA_REFERENCE, reference);
     crm_xml_add(request, PCMK__XA_CRM_TASK, task);
     crm_xml_add(request, PCMK__XA_CRM_SYS_TO, sys_to);
     crm_xml_add(request, PCMK__XA_CRM_SYS_FROM, true_from);
 
     /* HOSTTO will be ignored if it is to the DC anyway. */
     if (host_to != NULL && strlen(host_to) > 0) {
         crm_xml_add(request, PCMK__XA_CRM_HOST_TO, host_to);
     }
 
     if (msg_data != NULL) {
         xmlNode *wrapper = pcmk__xe_create(request, PCMK__XE_CRM_XML);
 
         pcmk__xml_copy(wrapper, msg_data);
     }
     free(reference);
     free(true_from);
 
     return request;
 }
 
 /*!
  * \brief Create a Pacemaker reply (for IPC or cluster layer)
  *
  * \param[in] original_request   XML of request this is a reply to
  * \param[in] xml_response_data  XML to copy as data section of reply
  * \param[in] origin             Name of function that called this one
  *
  * \return XML of new reply
  *
  * \note This function should not be called directly, but via the
  *       create_reply() wrapper.
  * \note The caller is responsible for freeing the return value using
  *       \c pcmk__xml_free().
  */
 xmlNode *
 create_reply_adv(const xmlNode *original_request, xmlNode *xml_response_data,
                  const char *origin)
 {
     xmlNode *reply = NULL;
 
     const char *host_from = crm_element_value(original_request, PCMK__XA_SRC);
     const char *sys_from = crm_element_value(original_request,
                                              PCMK__XA_CRM_SYS_FROM);
     const char *sys_to = crm_element_value(original_request,
                                            PCMK__XA_CRM_SYS_TO);
     const char *type = crm_element_value(original_request, PCMK__XA_SUBT);
     const char *operation = crm_element_value(original_request,
                                               PCMK__XA_CRM_TASK);
     const char *crm_msg_reference = crm_element_value(original_request,
                                                       PCMK_XA_REFERENCE);
 
     if (type == NULL) {
         crm_err("Cannot create new_message, no message type in original message");
         CRM_ASSERT(type != NULL);
         return NULL;
     }
 
     if (strcmp(type, PCMK__VALUE_REQUEST) != 0) {
         /* Replies should only be generated for request messages, but it's possible
          * we expect replies to other messages right now so this can't be enforced.
          */
         crm_trace("Creating a reply for a non-request original message");
     }
 
     reply = pcmk__xe_create(NULL, __func__);
     crm_xml_add(reply, PCMK_XA_ORIGIN, origin);
     crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_CRMD);
     crm_xml_add(reply, PCMK_XA_VERSION, CRM_FEATURE_SET);
     crm_xml_add(reply, PCMK__XA_SUBT, PCMK__VALUE_RESPONSE);
     crm_xml_add(reply, PCMK_XA_REFERENCE, crm_msg_reference);
     crm_xml_add(reply, PCMK__XA_CRM_TASK, operation);
 
     /* since this is a reply, we reverse the from and to */
     crm_xml_add(reply, PCMK__XA_CRM_SYS_TO, sys_from);
     crm_xml_add(reply, PCMK__XA_CRM_SYS_FROM, sys_to);
 
     /* HOSTTO will be ignored if it is to the DC anyway. */
     if (host_from != NULL && strlen(host_from) > 0) {
         crm_xml_add(reply, PCMK__XA_CRM_HOST_TO, host_from);
     }
 
     if (xml_response_data != NULL) {
         xmlNode *wrapper = pcmk__xe_create(reply, PCMK__XE_CRM_XML);
 
         pcmk__xml_copy(wrapper, xml_response_data);
     }
 
     return reply;
 }
 
 /*!
  * \brief Get name to be used as identifier for cluster messages
  *
  * \param[in] name  Actual system name to check
  *
  * \return Non-NULL cluster message identifier corresponding to name
  *
  * \note The Pacemaker daemons were renamed in version 2.0.0, but the old names
  *       must continue to be used as the identifier for cluster messages, so
  *       that mixed-version clusters are possible during a rolling upgrade.
  */
 const char *
 pcmk__message_name(const char *name)
 {
     if (name == NULL) {
         return "unknown";
 
     } else if (!strcmp(name, "pacemaker-attrd")) {
         return "attrd";
 
     } else if (!strcmp(name, "pacemaker-based")) {
         return CRM_SYSTEM_CIB;
 
     } else if (!strcmp(name, "pacemaker-controld")) {
         return CRM_SYSTEM_CRMD;
 
     } else if (!strcmp(name, "pacemaker-execd")) {
         return CRM_SYSTEM_LRMD;
 
     } else if (!strcmp(name, "pacemaker-fenced")) {
         return "stonith-ng";
 
     } else if (!strcmp(name, "pacemaker-schedulerd")) {
         return CRM_SYSTEM_PENGINE;
 
     } else {
         return name;
     }
 }
 
 /*!
  * \internal
  * \brief Register handlers for server commands
  *
  * \param[in] handlers  Array of handler functions for supported server commands
  *                      (the final entry must have a NULL command name, and if
  *                      it has a handler it will be used as the default handler
  *                      for unrecognized commands)
  *
  * \return Newly created hash table with commands and handlers
  * \note The caller is responsible for freeing the return value with
  *       g_hash_table_destroy().
  */
 GHashTable *
 pcmk__register_handlers(const pcmk__server_command_t handlers[])
 {
     GHashTable *commands = g_hash_table_new(g_str_hash, g_str_equal);
 
     if (handlers != NULL) {
         int i;
 
         for (i = 0; handlers[i].command != NULL; ++i) {
             g_hash_table_insert(commands, (gpointer) handlers[i].command,
                                 handlers[i].handler);
         }
         if (handlers[i].handler != NULL) {
             // g_str_hash() can't handle NULL, so use empty string for default
             g_hash_table_insert(commands, (gpointer) "", handlers[i].handler);
         }
     }
     return commands;
 }
 
 /*!
  * \internal
  * \brief Process an incoming request
  *
  * \param[in,out] request   Request to process
  * \param[in]     handlers  Command table created by pcmk__register_handlers()
  *
  * \return XML to send as reply (or NULL if no reply is needed)
  */
 xmlNode *
 pcmk__process_request(pcmk__request_t *request, GHashTable *handlers)
 {
     xmlNode *(*handler)(pcmk__request_t *request) = NULL;
 
     CRM_CHECK((request != NULL) && (request->op != NULL) && (handlers != NULL),
               return NULL);
 
     if (pcmk_is_set(request->flags, pcmk__request_sync)
         && (request->ipc_client != NULL)) {
         CRM_CHECK(request->ipc_client->request_id == request->ipc_id,
                   return NULL);
     }
 
     handler = g_hash_table_lookup(handlers, request->op);
     if (handler == NULL) {
         handler = g_hash_table_lookup(handlers, ""); // Default handler
         if (handler == NULL) {
             crm_info("Ignoring %s request from %s %s with no handler",
                      request->op, pcmk__request_origin_type(request),
                      pcmk__request_origin(request));
             return NULL;
         }
     }
 
     return (*handler)(request);
 }
 
 /*!
  * \internal
  * \brief Free memory used within a request (but not the request itself)
  *
  * \param[in,out] request  Request to reset
  */
 void
 pcmk__reset_request(pcmk__request_t *request)
 {
     free(request->op);
     request->op = NULL;
 
     pcmk__reset_result(&(request->result));
 }
-
-// Deprecated functions kept only for backward API compatibility
-// LCOV_EXCL_START
-
-#include <crm/common/xml_compat.h>
-
-gboolean
-add_message_xml(xmlNode *msg, const char *field, xmlNode *xml)
-{
-    xmlNode *holder = pcmk__xe_create(msg, field);
-
-    pcmk__xml_copy(holder, xml);
-    return TRUE;
-}
-
-xmlNode *
-get_message_xml(const xmlNode *msg, const char *field)
-{
-    xmlNode *child = pcmk__xe_first_child(msg, field, NULL, NULL);
-
-    return pcmk__xe_first_child(child, NULL, NULL, NULL);
-}
-
-// LCOV_EXCL_STOP
-// End deprecated API
diff --git a/lib/common/patchset.c b/lib/common/patchset.c
index 312bdd2af7..9c155674d2 100644
--- a/lib/common/patchset.c
+++ b/lib/common/patchset.c
@@ -1,1596 +1,1473 @@
 /*
  * 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 <crm_internal.h>
 
 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <time.h>
 #include <string.h>
 #include <stdlib.h>
 #include <stdarg.h>
 #include <bzlib.h>
 
 #include <libxml/tree.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>  // CRM_XML_LOG_BASE, etc.
 #include "crmcommon_private.h"
 
 /* Add changes for specified XML to patchset.
  * For patchset format, refer to diff schema.
  */
 static void
 add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset)
 {
     xmlNode *cIter = NULL;
     xmlAttr *pIter = NULL;
     xmlNode *change = NULL;
     xml_node_private_t *nodepriv = xml->_private;
     const char *value = NULL;
 
     if (nodepriv == NULL) {
         /* Elements that shouldn't occur in a CIB don't have _private set. They
          * should be stripped out, ignored, or have an error thrown by any code
          * that processes their parent, so we ignore any changes to them.
          */
         return;
     }
 
     // If this XML node is new, just report that
     if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
         GString *xpath = pcmk__element_xpath(xml->parent);
 
         if (xpath != NULL) {
             int position = pcmk__xml_position(xml, pcmk__xf_deleted);
 
             change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
 
             crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_CREATE);
             crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
             crm_xml_add_int(change, PCMK_XE_POSITION, position);
             pcmk__xml_copy(change, xml);
             g_string_free(xpath, TRUE);
         }
 
         return;
     }
 
     // Check each of the XML node's attributes for changes
     for (pIter = pcmk__xe_first_attr(xml); pIter != NULL;
          pIter = pIter->next) {
         xmlNode *attr = NULL;
 
         nodepriv = pIter->_private;
         if (!pcmk_any_flags_set(nodepriv->flags, pcmk__xf_deleted|pcmk__xf_dirty)) {
             continue;
         }
 
         if (change == NULL) {
             GString *xpath = pcmk__element_xpath(xml);
 
             if (xpath != NULL) {
                 change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
 
                 crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MODIFY);
                 crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
 
                 change = pcmk__xe_create(change, PCMK_XE_CHANGE_LIST);
                 g_string_free(xpath, TRUE);
             }
         }
 
         attr = pcmk__xe_create(change, PCMK_XE_CHANGE_ATTR);
 
         crm_xml_add(attr, PCMK_XA_NAME, (const char *) pIter->name);
         if (nodepriv->flags & pcmk__xf_deleted) {
             crm_xml_add(attr, PCMK_XA_OPERATION, "unset");
 
         } else {
             crm_xml_add(attr, PCMK_XA_OPERATION, "set");
 
             value = pcmk__xml_attr_value(pIter);
             crm_xml_add(attr, PCMK_XA_VALUE, value);
         }
     }
 
     if (change) {
         xmlNode *result = NULL;
 
         change = pcmk__xe_create(change->parent, PCMK_XE_CHANGE_RESULT);
         result = pcmk__xe_create(change, (const char *)xml->name);
 
         for (pIter = pcmk__xe_first_attr(xml); pIter != NULL;
              pIter = pIter->next) {
             nodepriv = pIter->_private;
             if (!pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) {
                 value = crm_element_value(xml, (const char *) pIter->name);
                 crm_xml_add(result, (const char *)pIter->name, value);
             }
         }
     }
 
     // Now recursively do the same for each child node of this node
     for (cIter = pcmk__xml_first_child(xml); cIter != NULL;
          cIter = pcmk__xml_next(cIter)) {
         add_xml_changes_to_patchset(cIter, patchset);
     }
 
     nodepriv = xml->_private;
     if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) {
         GString *xpath = pcmk__element_xpath(xml);
 
         crm_trace("%s.%s moved to position %d",
                   xml->name, pcmk__xe_id(xml),
                   pcmk__xml_position(xml, pcmk__xf_skip));
 
         if (xpath != NULL) {
             change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
 
             crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MOVE);
             crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
             crm_xml_add_int(change, PCMK_XE_POSITION,
                             pcmk__xml_position(xml, pcmk__xf_deleted));
             g_string_free(xpath, TRUE);
         }
     }
 }
 
 static bool
 is_config_change(xmlNode *xml)
 {
     GList *gIter = NULL;
     xml_node_private_t *nodepriv = NULL;
     xml_doc_private_t *docpriv;
     xmlNode *config = pcmk__xe_first_child(xml, PCMK_XE_CONFIGURATION, NULL,
                                            NULL);
 
     if (config) {
         nodepriv = config->_private;
     }
     if ((nodepriv != NULL) && pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) {
         return TRUE;
     }
 
     if ((xml->doc != NULL) && (xml->doc->_private != NULL)) {
         docpriv = xml->doc->_private;
         for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) {
             pcmk__deleted_xml_t *deleted_obj = gIter->data;
 
             if (strstr(deleted_obj->path,
                        "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION) != NULL) {
                 return TRUE;
             }
         }
     }
     return FALSE;
 }
 
 // @COMPAT Remove when v1 patchsets are removed
 static void
 xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff,
                    gboolean changed)
 {
     int lpc = 0;
     xmlNode *cib = NULL;
     xmlNode *diff_child = NULL;
 
     const char *tag = NULL;
 
     const char *vfields[] = {
         PCMK_XA_ADMIN_EPOCH,
         PCMK_XA_EPOCH,
         PCMK_XA_NUM_UPDATES,
     };
 
     if (local_diff == NULL) {
         crm_trace("Nothing to do");
         return;
     }
 
     tag = PCMK__XE_DIFF_REMOVED;
     diff_child = pcmk__xe_first_child(local_diff, tag, NULL, NULL);
     if (diff_child == NULL) {
         diff_child = pcmk__xe_create(local_diff, tag);
     }
 
     tag = PCMK_XE_CIB;
     cib = pcmk__xe_first_child(diff_child, tag, NULL, NULL);
     if (cib == NULL) {
         cib = pcmk__xe_create(diff_child, tag);
     }
 
     for (lpc = 0; (last != NULL) && (lpc < PCMK__NELEM(vfields)); lpc++) {
         const char *value = crm_element_value(last, vfields[lpc]);
 
         crm_xml_add(diff_child, vfields[lpc], value);
         if (changed || lpc == 2) {
             crm_xml_add(cib, vfields[lpc], value);
         }
     }
 
     tag = PCMK__XE_DIFF_ADDED;
     diff_child = pcmk__xe_first_child(local_diff, tag, NULL, NULL);
     if (diff_child == NULL) {
         diff_child = pcmk__xe_create(local_diff, tag);
     }
 
     tag = PCMK_XE_CIB;
     cib = pcmk__xe_first_child(diff_child, tag, NULL, NULL);
     if (cib == NULL) {
         cib = pcmk__xe_create(diff_child, tag);
     }
 
     for (lpc = 0; next && lpc < PCMK__NELEM(vfields); lpc++) {
         const char *value = crm_element_value(next, vfields[lpc]);
 
         crm_xml_add(diff_child, vfields[lpc], value);
     }
 
     for (xmlAttrPtr a = pcmk__xe_first_attr(next); a != NULL; a = a->next) {
         
         const char *p_value = pcmk__xml_attr_value(a);
 
         xmlSetProp(cib, a->name, (pcmkXmlStr) p_value);
     }
 
     crm_log_xml_explicit(local_diff, "Repaired-diff");
 }
 
 // @COMPAT Remove when v1 patchsets are removed
 static xmlNode *
 xml_create_patchset_v1(xmlNode *source, xmlNode *target, bool config,
                        bool suppress)
 {
     xmlNode *patchset = pcmk__diff_v1_xml_object(source, target, suppress);
 
     if (patchset) {
         CRM_LOG_ASSERT(xml_document_dirty(target));
         xml_repair_v1_diff(source, target, patchset, config);
         crm_xml_add(patchset, PCMK_XA_FORMAT, "1");
     }
     return patchset;
 }
 
 static xmlNode *
 xml_create_patchset_v2(xmlNode *source, xmlNode *target)
 {
     int lpc = 0;
     GList *gIter = NULL;
     xml_doc_private_t *docpriv;
 
     xmlNode *v = NULL;
     xmlNode *version = NULL;
     xmlNode *patchset = NULL;
     const char *vfields[] = {
         PCMK_XA_ADMIN_EPOCH,
         PCMK_XA_EPOCH,
         PCMK_XA_NUM_UPDATES,
     };
 
     CRM_ASSERT(target);
     if (!xml_document_dirty(target)) {
         return NULL;
     }
 
     CRM_ASSERT(target->doc);
     docpriv = target->doc->_private;
 
     patchset = pcmk__xe_create(NULL, PCMK_XE_DIFF);
     crm_xml_add_int(patchset, PCMK_XA_FORMAT, 2);
 
     version = pcmk__xe_create(patchset, PCMK_XE_VERSION);
 
     v = pcmk__xe_create(version, PCMK_XE_SOURCE);
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         const char *value = crm_element_value(source, vfields[lpc]);
 
         if (value == NULL) {
             value = "1";
         }
         crm_xml_add(v, vfields[lpc], value);
     }
 
     v = pcmk__xe_create(version, PCMK_XE_TARGET);
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         const char *value = crm_element_value(target, vfields[lpc]);
 
         if (value == NULL) {
             value = "1";
         }
         crm_xml_add(v, vfields[lpc], value);
     }
 
     for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) {
         pcmk__deleted_xml_t *deleted_obj = gIter->data;
         xmlNode *change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
 
         crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_DELETE);
         crm_xml_add(change, PCMK_XA_PATH, deleted_obj->path);
         if (deleted_obj->position >= 0) {
             crm_xml_add_int(change, PCMK_XE_POSITION, deleted_obj->position);
         }
     }
 
     add_xml_changes_to_patchset(target, patchset);
     return patchset;
 }
 
 xmlNode *
 xml_create_patchset(int format, xmlNode *source, xmlNode *target,
                     bool *config_changed, bool manage_version)
 {
     int counter = 0;
     bool config = FALSE;
     xmlNode *patch = NULL;
     const char *version = crm_element_value(source, PCMK_XA_CRM_FEATURE_SET);
 
     xml_acl_disable(target);
     if (!xml_document_dirty(target)) {
         crm_trace("No change %d", format);
         return NULL; /* No change */
     }
 
     config = is_config_change(target);
     if (config_changed) {
         *config_changed = config;
     }
 
     if (manage_version && config) {
         crm_trace("Config changed %d", format);
         crm_xml_add(target, PCMK_XA_NUM_UPDATES, "0");
 
         crm_element_value_int(target, PCMK_XA_EPOCH, &counter);
         crm_xml_add_int(target, PCMK_XA_EPOCH, counter+1);
 
     } else if (manage_version) {
         crm_element_value_int(target, PCMK_XA_NUM_UPDATES, &counter);
         crm_trace("Status changed %d - %d %s", format, counter,
                   crm_element_value(source, PCMK_XA_NUM_UPDATES));
         crm_xml_add_int(target, PCMK_XA_NUM_UPDATES, (counter + 1));
     }
 
     if (format == 0) {
         if (compare_version("3.0.8", version) < 0) {
             format = 2;
         } else {
             format = 1;
         }
         crm_trace("Using patch format %d for version: %s", format, version);
     }
 
     switch (format) {
         case 1:
             // @COMPAT Remove when v1 patchsets are removed
             patch = xml_create_patchset_v1(source, target, config, FALSE);
             break;
         case 2:
             patch = xml_create_patchset_v2(source, target);
             break;
         default:
             crm_err("Unknown patch format: %d", format);
             return NULL;
     }
     return patch;
 }
 
 void
 patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target,
                         bool with_digest)
 {
     int format = 1;
     const char *version = NULL;
     char *digest = NULL;
 
     if ((patch == NULL) || (source == NULL) || (target == NULL)) {
         return;
     }
 
     /* We should always call xml_accept_changes() before calculating a digest.
      * Otherwise, with an on-tracking dirty target, we could get a wrong digest.
      */
     CRM_LOG_ASSERT(!xml_document_dirty(target));
 
     crm_element_value_int(patch, PCMK_XA_FORMAT, &format);
     if ((format > 1) && !with_digest) {
         return;
     }
 
     version = crm_element_value(source, PCMK_XA_CRM_FEATURE_SET);
     digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version);
 
     crm_xml_add(patch, PCMK__XA_DIGEST, digest);
     free(digest);
 
     return;
 }
 
 // @COMPAT Remove when v1 patchsets are removed
 static xmlNode *
 subtract_v1_xml_comment(xmlNode *parent, xmlNode *left, xmlNode *right,
                         gboolean *changed)
 {
     CRM_CHECK(left != NULL, return NULL);
     CRM_CHECK(left->type == XML_COMMENT_NODE, return NULL);
 
     if ((right == NULL) || !pcmk__str_eq((const char *)left->content,
                                          (const char *)right->content,
                                          pcmk__str_casei)) {
         xmlNode *deleted = NULL;
 
         deleted = pcmk__xml_copy(parent, left);
         *changed = TRUE;
 
         return deleted;
     }
 
     return NULL;
 }
 
 // @COMPAT Remove when v1 patchsets are removed
 static xmlNode *
 subtract_v1_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right,
                        bool full, gboolean *changed, const char *marker)
 {
     gboolean dummy = FALSE;
     xmlNode *diff = NULL;
     xmlNode *right_child = NULL;
     xmlNode *left_child = NULL;
     xmlAttrPtr xIter = NULL;
 
     const char *id = NULL;
     const char *name = NULL;
     const char *value = NULL;
     const char *right_val = NULL;
 
     if (changed == NULL) {
         changed = &dummy;
     }
 
     if (left == NULL) {
         return NULL;
     }
 
     if (left->type == XML_COMMENT_NODE) {
         return subtract_v1_xml_comment(parent, left, right, changed);
     }
 
     id = pcmk__xe_id(left);
     name = (const char *) left->name;
     if (right == NULL) {
         xmlNode *deleted = NULL;
 
         crm_trace("Processing <%s " PCMK_XA_ID "=%s> (complete copy)",
                   name, id);
         deleted = pcmk__xml_copy(parent, left);
         crm_xml_add(deleted, PCMK__XA_CRM_DIFF_MARKER, marker);
 
         *changed = TRUE;
         return deleted;
     }
 
     CRM_CHECK(name != NULL, return NULL);
     CRM_CHECK(pcmk__xe_is(left, (const char *) right->name), return NULL);
 
     // Check for PCMK__XA_CRM_DIFF_MARKER in a child
     value = crm_element_value(right, PCMK__XA_CRM_DIFF_MARKER);
     if ((value != NULL) && (strcmp(value, "removed:top") == 0)) {
         crm_trace("We are the root of the deletion: %s.id=%s", name, id);
         *changed = TRUE;
         return NULL;
     }
 
     // @TODO Avoiding creating the full hierarchy would save work here
     diff = pcmk__xe_create(parent, name);
 
     // Changes to child objects
     for (left_child = pcmk__xml_first_child(left); left_child != NULL;
          left_child = pcmk__xml_next(left_child)) {
         gboolean child_changed = FALSE;
 
         right_child = pcmk__xml_match(right, left_child, false);
         subtract_v1_xml_object(diff, left_child, right_child, full,
                                &child_changed, marker);
         if (child_changed) {
             *changed = TRUE;
         }
     }
 
     if (!*changed) {
         /* Nothing to do */
 
     } else if (full) {
         xmlAttrPtr pIter = NULL;
 
         for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
              pIter = pIter->next) {
             const char *p_name = (const char *)pIter->name;
             const char *p_value = pcmk__xml_attr_value(pIter);
 
             xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value);
         }
 
         // We have everything we need
         goto done;
     }
 
     // Changes to name/value pairs
     for (xIter = pcmk__xe_first_attr(left); xIter != NULL;
          xIter = xIter->next) {
         const char *prop_name = (const char *) xIter->name;
         xmlAttrPtr right_attr = NULL;
         xml_node_private_t *nodepriv = NULL;
 
         if (strcmp(prop_name, PCMK_XA_ID) == 0) {
             // id already obtained when present ~ this case, so just reuse
             xmlSetProp(diff, (pcmkXmlStr) PCMK_XA_ID, (pcmkXmlStr) id);
             continue;
         }
 
         if (pcmk__xa_filterable(prop_name)) {
             continue;
         }
 
         right_attr = xmlHasProp(right, (pcmkXmlStr) prop_name);
         if (right_attr) {
             nodepriv = right_attr->_private;
         }
 
         right_val = crm_element_value(right, prop_name);
         if ((right_val == NULL) || (nodepriv && pcmk_is_set(nodepriv->flags, pcmk__xf_deleted))) {
             /* new */
             *changed = TRUE;
             if (full) {
                 xmlAttrPtr pIter = NULL;
 
                 for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
                      pIter = pIter->next) {
                     const char *p_name = (const char *) pIter->name;
                     const char *p_value = pcmk__xml_attr_value(pIter);
 
                     xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value);
                 }
                 break;
 
             } else {
                 const char *left_value = pcmk__xml_attr_value(xIter);
 
                 xmlSetProp(diff, (pcmkXmlStr) prop_name, (pcmkXmlStr) value);
                 crm_xml_add(diff, prop_name, left_value);
             }
 
         } else {
             /* Only now do we need the left value */
             const char *left_value = pcmk__xml_attr_value(xIter);
 
             if (strcmp(left_value, right_val) == 0) {
                 /* unchanged */
 
             } else {
                 *changed = TRUE;
                 if (full) {
                     xmlAttrPtr pIter = NULL;
 
                     crm_trace("Changes detected to %s in "
                               "<%s " PCMK_XA_ID "=%s>", prop_name, name, id);
                     for (pIter = pcmk__xe_first_attr(left); pIter != NULL;
                          pIter = pIter->next) {
                         const char *p_name = (const char *) pIter->name;
                         const char *p_value = pcmk__xml_attr_value(pIter);
 
                         xmlSetProp(diff, (pcmkXmlStr) p_name,
                                    (pcmkXmlStr) p_value);
                     }
                     break;
 
                 } else {
                     crm_trace("Changes detected to %s (%s -> %s) in "
                               "<%s " PCMK_XA_ID "=%s>",
                               prop_name, left_value, right_val, name, id);
                     crm_xml_add(diff, prop_name, left_value);
                 }
             }
         }
     }
 
     if (!*changed) {
         pcmk__xml_free(diff);
         return NULL;
 
     } else if (!full && (id != NULL)) {
         crm_xml_add(diff, PCMK_XA_ID, id);
     }
   done:
     return diff;
 }
 
 /* @COMPAT Remove when v1 patchsets are removed.
  *
  * Return true if attribute name is not \c PCMK_XML_ID.
  */
 static bool
 not_id(xmlAttrPtr attr, void *user_data)
 {
     return strcmp((const char *) attr->name, PCMK_XA_ID) != 0;
 }
 
 /* @COMPAT Remove when v1 patchsets are removed.
  *
  * Apply the removals section of a v1 patchset to an XML node.
  */
 static void
 process_v1_removals(xmlNode *target, xmlNode *patch)
 {
     xmlNode *patch_child = NULL;
     xmlNode *cIter = NULL;
 
     char *id = NULL;
     const char *value = NULL;
 
     if ((target == NULL) || (patch == NULL)) {
         return;
     }
 
     if (target->type == XML_COMMENT_NODE) {
         gboolean dummy;
 
         subtract_v1_xml_comment(target->parent, target, patch, &dummy);
     }
 
     CRM_CHECK(pcmk__xe_is(target, (const char *) patch->name), return);
     CRM_CHECK(pcmk__str_eq(pcmk__xe_id(target), pcmk__xe_id(patch),
                            pcmk__str_none),
               return);
 
     // Check for PCMK__XA_CRM_DIFF_MARKER in a child
     id = crm_element_value_copy(target, PCMK_XA_ID);
     value = crm_element_value(patch, PCMK__XA_CRM_DIFF_MARKER);
     if ((value != NULL) && (strcmp(value, "removed:top") == 0)) {
         crm_trace("We are the root of the deletion: %s.id=%s",
                   target->name, id);
         pcmk__xml_free(target);
         free(id);
         return;
     }
 
     // Removing then restoring id would change ordering of properties
     pcmk__xe_remove_matching_attrs(patch, not_id, NULL);
 
     // Changes to child objects
     cIter = pcmk__xml_first_child(target);
     while (cIter) {
         xmlNode *target_child = cIter;
 
         cIter = pcmk__xml_next(cIter);
         patch_child = pcmk__xml_match(patch, target_child, false);
         process_v1_removals(target_child, patch_child);
     }
     free(id);
 }
 
 /* @COMPAT Remove when v1 patchsets are removed.
  *
  * Apply the additions section of a v1 patchset to an XML node.
  */
 static void
 process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch)
 {
     xmlNode *patch_child = NULL;
     xmlNode *target_child = NULL;
     xmlAttrPtr xIter = NULL;
 
     const char *id = NULL;
     const char *name = NULL;
     const char *value = NULL;
 
     if (patch == NULL) {
         return;
     } else if ((parent == NULL) && (target == NULL)) {
         return;
     }
 
     // Check for PCMK__XA_CRM_DIFF_MARKER in a child
     name = (const char *) patch->name;
     value = crm_element_value(patch, PCMK__XA_CRM_DIFF_MARKER);
     if ((target == NULL) && (value != NULL)
         && (strcmp(value, "added:top") == 0)) {
         id = pcmk__xe_id(patch);
         crm_trace("We are the root of the addition: %s.id=%s", name, id);
         pcmk__xml_copy(parent, patch);
         return;
 
     } else if (target == NULL) {
         id = pcmk__xe_id(patch);
         crm_err("Could not locate: %s.id=%s", name, id);
         return;
     }
 
     if (target->type == XML_COMMENT_NODE) {
         pcmk__xc_update(parent, target, patch);
     }
 
     CRM_CHECK(pcmk__xe_is(target, name), return);
     CRM_CHECK(pcmk__str_eq(pcmk__xe_id(target), pcmk__xe_id(patch),
                            pcmk__str_none),
               return);
 
     for (xIter = pcmk__xe_first_attr(patch); xIter != NULL;
          xIter = xIter->next) {
         const char *p_name = (const char *) xIter->name;
         const char *p_value = pcmk__xml_attr_value(xIter);
 
         pcmk__xe_remove_attr(target, p_name);   // Preserve patch order
         crm_xml_add(target, p_name, p_value);
     }
 
     // Changes to child objects
     for (patch_child = pcmk__xml_first_child(patch); patch_child != NULL;
          patch_child = pcmk__xml_next(patch_child)) {
 
         target_child = pcmk__xml_match(target, patch_child, false);
         process_v1_additions(target, target_child, patch_child);
     }
 }
 
 /*!
  * \internal
  * \brief Find additions or removals in a patch set
  *
  * \param[in]     patchset   XML of patch
  * \param[in]     format     Patch version
  * \param[in]     added      TRUE if looking for additions, FALSE if removals
  * \param[in,out] patch_node Will be set to node if found
  *
  * \return TRUE if format is valid, FALSE if invalid
  */
 static bool
 find_patch_xml_node(const xmlNode *patchset, int format, bool added,
                     xmlNode **patch_node)
 {
     xmlNode *cib_node;
     const char *label;
 
     switch (format) {
         case 1:
             // @COMPAT Remove when v1 patchsets are removed
             label = added? PCMK__XE_DIFF_ADDED : PCMK__XE_DIFF_REMOVED;
             *patch_node = pcmk__xe_first_child(patchset, label, NULL, NULL);
             cib_node = pcmk__xe_first_child(*patch_node, PCMK_XE_CIB, NULL,
                                             NULL);
             if (cib_node != NULL) {
                 *patch_node = cib_node;
             }
             break;
         case 2:
             label = added? PCMK_XE_TARGET : PCMK_XE_SOURCE;
             *patch_node = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL,
                                                NULL);
             *patch_node = pcmk__xe_first_child(*patch_node, label, NULL, NULL);
             break;
         default:
             crm_warn("Unknown patch format: %d", format);
             *patch_node = NULL;
             return FALSE;
     }
     return TRUE;
 }
 
 // Get CIB versions used for additions and deletions in a patchset
 bool
 xml_patch_versions(const xmlNode *patchset, int add[3], int del[3])
 {
     int lpc = 0;
     int format = 1;
     xmlNode *tmp = NULL;
 
     const char *vfields[] = {
         PCMK_XA_ADMIN_EPOCH,
         PCMK_XA_EPOCH,
         PCMK_XA_NUM_UPDATES,
     };
 
 
     crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
 
     /* Process removals */
     if (!find_patch_xml_node(patchset, format, FALSE, &tmp)) {
         return -EINVAL;
     }
     if (tmp != NULL) {
         for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
             crm_element_value_int(tmp, vfields[lpc], &(del[lpc]));
             crm_trace("Got %d for del[%s]", del[lpc], vfields[lpc]);
         }
     }
 
     /* Process additions */
     if (!find_patch_xml_node(patchset, format, TRUE, &tmp)) {
         return -EINVAL;
     }
     if (tmp != NULL) {
         for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
             crm_element_value_int(tmp, vfields[lpc], &(add[lpc]));
             crm_trace("Got %d for add[%s]", add[lpc], vfields[lpc]);
         }
     }
     return pcmk_ok;
 }
 
 /*!
  * \internal
  * \brief Check whether patchset can be applied to current CIB
  *
  * \param[in] xml       Root of current CIB
  * \param[in] patchset  Patchset to check
  *
  * \return Standard Pacemaker return code
  */
 static int
 xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset)
 {
     int lpc = 0;
     bool changed = FALSE;
 
     int this[] = { 0, 0, 0 };
     int add[] = { 0, 0, 0 };
     int del[] = { 0, 0, 0 };
 
     const char *vfields[] = {
         PCMK_XA_ADMIN_EPOCH,
         PCMK_XA_EPOCH,
         PCMK_XA_NUM_UPDATES,
     };
 
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         crm_element_value_int(xml, vfields[lpc], &(this[lpc]));
         crm_trace("Got %d for this[%s]", this[lpc], vfields[lpc]);
         if (this[lpc] < 0) {
             this[lpc] = 0;
         }
     }
 
     /* Set some defaults in case nothing is present */
     add[0] = this[0];
     add[1] = this[1];
     add[2] = this[2] + 1;
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         del[lpc] = this[lpc];
     }
 
     xml_patch_versions(patchset, add, del);
 
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         if (this[lpc] < del[lpc]) {
             crm_debug("Current %s is too low (%d.%d.%d < %d.%d.%d --> %d.%d.%d)",
                       vfields[lpc], this[0], this[1], this[2],
                       del[0], del[1], del[2], add[0], add[1], add[2]);
             return pcmk_rc_diff_resync;
 
         } else if (this[lpc] > del[lpc]) {
             crm_info("Current %s is too high (%d.%d.%d > %d.%d.%d --> %d.%d.%d) %p",
                      vfields[lpc], this[0], this[1], this[2],
                      del[0], del[1], del[2], add[0], add[1], add[2], patchset);
             crm_log_xml_info(patchset, "OldPatch");
             return pcmk_rc_old_data;
         }
     }
 
     for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
         if (add[lpc] > del[lpc]) {
             changed = TRUE;
         }
     }
 
     if (!changed) {
         crm_notice("Versions did not change in patch %d.%d.%d",
                    add[0], add[1], add[2]);
         return pcmk_rc_old_data;
     }
 
     crm_debug("Can apply patch %d.%d.%d to %d.%d.%d",
               add[0], add[1], add[2], this[0], this[1], this[2]);
     return pcmk_rc_ok;
 }
 
 // @COMPAT Remove when v1 patchsets are removed
 static void
 purge_v1_diff_markers(xmlNode *node)
 {
     xmlNode *child = NULL;
 
     CRM_CHECK(node != NULL, return);
 
     pcmk__xe_remove_attr(node, PCMK__XA_CRM_DIFF_MARKER);
     for (child = pcmk__xml_first_child(node); child != NULL;
          child = pcmk__xml_next(child)) {
         purge_v1_diff_markers(child);
     }
 }
 
 // @COMPAT Remove when v1 patchsets are removed
 /*!
  * \internal
  * \brief Apply a version 1 patchset to an XML node
  *
  * \param[in,out] xml       XML to apply patchset to
  * \param[in]     patchset  Patchset to apply
  *
  * \return Standard Pacemaker return code
  */
 static int
 apply_v1_patchset(xmlNode *xml, const xmlNode *patchset)
 {
     int rc = pcmk_rc_ok;
     int root_nodes_seen = 0;
 
     xmlNode *child_diff = NULL;
     xmlNode *added = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_ADDED, NULL,
                                           NULL);
     xmlNode *removed = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_REMOVED,
                                             NULL, NULL);
     xmlNode *old = pcmk__xml_copy(NULL, xml);
 
     crm_trace("Subtraction Phase");
     for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL;
          child_diff = pcmk__xml_next(child_diff)) {
         CRM_CHECK(root_nodes_seen == 0, rc = FALSE);
         if (root_nodes_seen == 0) {
             process_v1_removals(xml, child_diff);
         }
         root_nodes_seen++;
     }
 
     if (root_nodes_seen > 1) {
         crm_err("(-) Diffs cannot contain more than one change set... saw %d",
                 root_nodes_seen);
         rc = ENOTUNIQ;
     }
 
     root_nodes_seen = 0;
     crm_trace("Addition Phase");
     if (rc == pcmk_rc_ok) {
         xmlNode *child_diff = NULL;
 
         for (child_diff = pcmk__xml_first_child(added); child_diff != NULL;
              child_diff = pcmk__xml_next(child_diff)) {
             CRM_CHECK(root_nodes_seen == 0, rc = FALSE);
             if (root_nodes_seen == 0) {
                 process_v1_additions(NULL, xml, child_diff);
             }
             root_nodes_seen++;
         }
     }
 
     if (root_nodes_seen > 1) {
         crm_err("(+) Diffs cannot contain more than one change set... saw %d",
                 root_nodes_seen);
         rc = ENOTUNIQ;
     }
 
     purge_v1_diff_markers(xml); // Purge prior to checking digest
 
     pcmk__xml_free(old);
     return rc;
 }
 
 // Return first child matching element name and optionally id or position
 static xmlNode *
 first_matching_xml_child(const xmlNode *parent, const char *name,
                          const char *id, int position)
 {
     xmlNode *cIter = NULL;
 
     for (cIter = pcmk__xml_first_child(parent); cIter != NULL;
          cIter = pcmk__xml_next(cIter)) {
         if (strcmp((const char *) cIter->name, name) != 0) {
             continue;
         } else if (id) {
             const char *cid = pcmk__xe_id(cIter);
 
             if ((cid == NULL) || (strcmp(cid, id) != 0)) {
                 continue;
             }
         }
 
         // "position" makes sense only for XML comments for now
         if ((cIter->type == XML_COMMENT_NODE)
             && (position >= 0)
             && (pcmk__xml_position(cIter, pcmk__xf_skip) != position)) {
             continue;
         }
 
         return cIter;
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Simplified, more efficient alternative to get_xpath_object()
  *
  * \param[in] top              Root of XML to search
  * \param[in] key              Search xpath
  * \param[in] target_position  If deleting, where to delete
  *
  * \return XML child matching xpath if found, NULL otherwise
  *
  * \note This only works on simplified xpaths found in v2 patchset diffs,
  *       i.e. the only allowed search predicate is [@id='XXX'].
  */
 static xmlNode *
 search_v2_xpath(const xmlNode *top, const char *key, int target_position)
 {
     xmlNode *target = (xmlNode *) top->doc;
     const char *current = key;
     char *section;
     char *remainder;
     char *id;
     char *tag;
     char *path = NULL;
     int rc;
     size_t key_len;
 
     CRM_CHECK(key != NULL, return NULL);
     key_len = strlen(key);
 
     /* These are scanned from key after a slash, so they can't be bigger
      * than key_len - 1 characters plus a null terminator.
      */
 
     remainder = pcmk__assert_alloc(key_len, sizeof(char));
     section = pcmk__assert_alloc(key_len, sizeof(char));
     id = pcmk__assert_alloc(key_len, sizeof(char));
     tag = pcmk__assert_alloc(key_len, sizeof(char));
 
     do {
         // Look for /NEXT_COMPONENT/REMAINING_COMPONENTS
         rc = sscanf(current, "/%[^/]%s", section, remainder);
         if (rc > 0) {
             // Separate FIRST_COMPONENT into TAG[@id='ID']
             int f = sscanf(section, "%[^[][@" PCMK_XA_ID "='%[^']", tag, id);
             int current_position = -1;
 
             /* The target position is for the final component tag, so only use
              * it if there is nothing left to search after this component.
              */
             if ((rc == 1) && (target_position >= 0)) {
                 current_position = target_position;
             }
 
             switch (f) {
                 case 1:
                     // @COMPAT Remove when v1 patchsets are removed
                     target = first_matching_xml_child(target, tag, NULL,
                                                       current_position);
                     break;
                 case 2:
                     target = first_matching_xml_child(target, tag, id,
                                                       current_position);
                     break;
                 default:
                     // This should not be possible
                     target = NULL;
                     break;
             }
             current = remainder;
         }
 
     // Continue if something remains to search, and we've matched so far
     } while ((rc == 2) && target);
 
     if (target) {
         crm_trace("Found %s for %s",
                   (path = (char *) xmlGetNodePath(target)), key);
         free(path);
     } else {
         crm_debug("No match for %s", key);
     }
 
     free(remainder);
     free(section);
     free(tag);
     free(id);
     return target;
 }
 
 typedef struct xml_change_obj_s {
     const xmlNode *change;
     xmlNode *match;
 } xml_change_obj_t;
 
 static gint
 sort_change_obj_by_position(gconstpointer a, gconstpointer b)
 {
     const xml_change_obj_t *change_obj_a = a;
     const xml_change_obj_t *change_obj_b = b;
     int position_a = -1;
     int position_b = -1;
 
     crm_element_value_int(change_obj_a->change, PCMK_XE_POSITION, &position_a);
     crm_element_value_int(change_obj_b->change, PCMK_XE_POSITION, &position_b);
 
     if (position_a < position_b) {
         return -1;
 
     } else if (position_a > position_b) {
         return 1;
     }
 
     return 0;
 }
 
 /*!
  * \internal
  * \brief Apply a version 2 patchset to an XML node
  *
  * \param[in,out] xml       XML to apply patchset to
  * \param[in]     patchset  Patchset to apply
  *
  * \return Standard Pacemaker return code
  */
 static int
 apply_v2_patchset(xmlNode *xml, const xmlNode *patchset)
 {
     int rc = pcmk_rc_ok;
     const xmlNode *change = NULL;
     GList *change_objs = NULL;
     GList *gIter = NULL;
 
     for (change = pcmk__xml_first_child(patchset); change != NULL;
          change = pcmk__xml_next(change)) {
         xmlNode *match = NULL;
         const char *op = crm_element_value(change, PCMK_XA_OPERATION);
         const char *xpath = crm_element_value(change, PCMK_XA_PATH);
         int position = -1;
 
         if (op == NULL) {
             continue;
         }
 
         crm_trace("Processing %s %s", change->name, op);
 
         /* PCMK_VALUE_DELETE changes for XML comments are generated with
          * PCMK_XE_POSITION
          */
         if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
             crm_element_value_int(change, PCMK_XE_POSITION, &position);
         }
         match = search_v2_xpath(xml, xpath, position);
         crm_trace("Performing %s on %s with %p", op, xpath, match);
 
         if ((match == NULL) && (strcmp(op, PCMK_VALUE_DELETE) == 0)) {
             crm_debug("No %s match for %s in %p", op, xpath, xml->doc);
             continue;
 
         } else if (match == NULL) {
             crm_err("No %s match for %s in %p", op, xpath, xml->doc);
             rc = pcmk_rc_diff_failed;
             continue;
 
         } else if (pcmk__str_any_of(op,
                                     PCMK_VALUE_CREATE, PCMK_VALUE_MOVE, NULL)) {
             // Delay the adding of a PCMK_VALUE_CREATE object
             xml_change_obj_t *change_obj =
                 pcmk__assert_alloc(1, sizeof(xml_change_obj_t));
 
             change_obj->change = change;
             change_obj->match = match;
 
             change_objs = g_list_append(change_objs, change_obj);
 
             if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
                 // Temporarily put the PCMK_VALUE_MOVE object after the last sibling
                 if ((match->parent != NULL) && (match->parent->last != NULL)) {
                     xmlAddNextSibling(match->parent->last, match);
                 }
             }
 
         } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
             pcmk__xml_free(match);
 
         } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
             const xmlNode *child = pcmk__xe_first_child(change,
                                                         PCMK_XE_CHANGE_RESULT,
                                                         NULL, NULL);
             const xmlNode *attrs = pcmk__xml_first_child(child);
 
             if (attrs == NULL) {
                 rc = ENOMSG;
                 continue;
             }
             pcmk__xe_remove_matching_attrs(match, NULL, NULL); // Remove all
 
             for (xmlAttrPtr pIter = pcmk__xe_first_attr(attrs); pIter != NULL;
                  pIter = pIter->next) {
                 const char *name = (const char *) pIter->name;
                 const char *value = pcmk__xml_attr_value(pIter);
 
                 crm_xml_add(match, name, value);
             }
 
         } else {
             crm_err("Unknown operation: %s", op);
             rc = pcmk_rc_diff_failed;
         }
     }
 
     // Changes should be generated in the right order. Double checking.
     change_objs = g_list_sort(change_objs, sort_change_obj_by_position);
 
     for (gIter = change_objs; gIter; gIter = gIter->next) {
         xml_change_obj_t *change_obj = gIter->data;
         xmlNode *match = change_obj->match;
         const char *op = NULL;
         const char *xpath = NULL;
 
         change = change_obj->change;
 
         op = crm_element_value(change, PCMK_XA_OPERATION);
         xpath = crm_element_value(change, PCMK_XA_PATH);
 
         crm_trace("Continue performing %s on %s with %p", op, xpath, match);
 
         if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
             int position = 0;
             xmlNode *child = NULL;
             xmlNode *match_child = NULL;
 
             match_child = match->children;
             crm_element_value_int(change, PCMK_XE_POSITION, &position);
 
             while ((match_child != NULL)
                    && (position != pcmk__xml_position(match_child, pcmk__xf_skip))) {
                 match_child = match_child->next;
             }
 
             child = xmlDocCopyNode(change->children, match->doc, 1);
             if (child == NULL) {
                 return ENOMEM;
             }
 
             if (match_child) {
                 crm_trace("Adding %s at position %d", child->name, position);
                 xmlAddPrevSibling(match_child, child);
 
             } else if (match->last) {
                 crm_trace("Adding %s at position %d (end)",
                           child->name, position);
                 xmlAddNextSibling(match->last, child);
 
             } else {
                 crm_trace("Adding %s at position %d (first)",
                           child->name, position);
                 CRM_LOG_ASSERT(position == 0);
                 xmlAddChild(match, child);
             }
             pcmk__xml_mark_created(child);
 
         } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
             int position = 0;
 
             crm_element_value_int(change, PCMK_XE_POSITION, &position);
             if (position != pcmk__xml_position(match, pcmk__xf_skip)) {
                 xmlNode *match_child = NULL;
                 int p = position;
 
                 if (p > pcmk__xml_position(match, pcmk__xf_skip)) {
                     p++; // Skip ourselves
                 }
 
                 CRM_ASSERT(match->parent != NULL);
                 match_child = match->parent->children;
 
                 while ((match_child != NULL)
                        && (p != pcmk__xml_position(match_child, pcmk__xf_skip))) {
                     match_child = match_child->next;
                 }
 
                 crm_trace("Moving %s to position %d (was %d, prev %p, %s %p)",
                           match->name, position,
                           pcmk__xml_position(match, pcmk__xf_skip),
                           match->prev, (match_child? "next":"last"),
                           (match_child? match_child : match->parent->last));
 
                 if (match_child) {
                     xmlAddPrevSibling(match_child, match);
 
                 } else {
                     CRM_ASSERT(match->parent->last != NULL);
                     xmlAddNextSibling(match->parent->last, match);
                 }
 
             } else {
                 crm_trace("%s is already in position %d",
                           match->name, position);
             }
 
             if (position != pcmk__xml_position(match, pcmk__xf_skip)) {
                 crm_err("Moved %s.%s to position %d instead of %d (%p)",
                         match->name, pcmk__xe_id(match),
                         pcmk__xml_position(match, pcmk__xf_skip),
                         position, match->prev);
                 rc = pcmk_rc_diff_failed;
             }
         }
     }
 
     g_list_free_full(change_objs, free);
     return rc;
 }
 
 int
 xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version)
 {
     int format = 1;
     int rc = pcmk_ok;
     xmlNode *old = NULL;
     const char *digest = NULL;
 
     if (patchset == NULL) {
         return rc;
     }
 
     pcmk__log_xml_patchset(LOG_TRACE, patchset);
 
     if (check_version) {
         rc = pcmk_rc2legacy(xml_patch_version_check(xml, patchset));
         if (rc != pcmk_ok) {
             return rc;
         }
     }
 
     digest = crm_element_value(patchset, PCMK__XA_DIGEST);
     if (digest != NULL) {
         /* Make original XML available for logging in case result doesn't have
          * expected digest
          */
         pcmk__if_tracing(old = pcmk__xml_copy(NULL, xml), {});
     }
 
     if (rc == pcmk_ok) {
         crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
         switch (format) {
             case 1:
                 // @COMPAT Remove when v1 patchsets are removed
                 rc = pcmk_rc2legacy(apply_v1_patchset(xml, patchset));
                 break;
             case 2:
                 rc = pcmk_rc2legacy(apply_v2_patchset(xml, patchset));
                 break;
             default:
                 crm_err("Unknown patch format: %d", format);
                 rc = -EINVAL;
         }
     }
 
     if ((rc == pcmk_ok) && (digest != NULL)) {
         char *new_digest = NULL;
         char *version = crm_element_value_copy(xml, PCMK_XA_CRM_FEATURE_SET);
 
         new_digest = calculate_xml_versioned_digest(xml, FALSE, TRUE, version);
         if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) {
             crm_info("v%d digest mis-match: expected %s, calculated %s",
                      format, digest, new_digest);
             rc = -pcmk_err_diff_failed;
             pcmk__if_tracing(
                 {
                     save_xml_to_file(old, "PatchDigest:input", NULL);
                     save_xml_to_file(xml, "PatchDigest:result", NULL);
                     save_xml_to_file(patchset, "PatchDigest:diff", NULL);
                 },
                 {}
             );
 
         } else {
             crm_trace("v%d digest matched: expected %s, calculated %s",
                       format, digest, new_digest);
         }
         free(new_digest);
         free(version);
     }
     pcmk__xml_free(old);
     return rc;
 }
 
 // @COMPAT Remove when v1 patchsets are removed
 static bool
 can_prune_leaf_v1(xmlNode *node)
 {
     xmlNode *cIter = NULL;
     bool can_prune = true;
 
     CRM_CHECK(node != NULL, return false);
 
     /* @COMPAT PCMK__XE_ROLE_REF was deprecated in Pacemaker 1.1.12 (needed for
      * rolling upgrades)
      */
     if (pcmk__strcase_any_of((const char *) node->name,
                              PCMK_XE_RESOURCE_REF, PCMK_XE_OBJ_REF,
                              PCMK_XE_ROLE, PCMK__XE_ROLE_REF,
                              NULL)) {
         return false;
     }
 
     for (xmlAttrPtr a = pcmk__xe_first_attr(node); a != NULL; a = a->next) {
         const char *p_name = (const char *) a->name;
 
         if (strcmp(p_name, PCMK_XA_ID) == 0) {
             continue;
         }
         can_prune = false;
     }
 
     cIter = pcmk__xml_first_child(node);
     while (cIter) {
         xmlNode *child = cIter;
 
         cIter = pcmk__xml_next(cIter);
         if (can_prune_leaf_v1(child)) {
             pcmk__xml_free(child);
         } else {
             can_prune = false;
         }
     }
     return can_prune;
 }
 
 // @COMPAT Remove when v1 patchsets are removed
 xmlNode *
 pcmk__diff_v1_xml_object(xmlNode *old, xmlNode *new, bool suppress)
 {
     xmlNode *tmp1 = NULL;
     xmlNode *diff = pcmk__xe_create(NULL, PCMK_XE_DIFF);
     xmlNode *removed = pcmk__xe_create(diff, PCMK__XE_DIFF_REMOVED);
     xmlNode *added = pcmk__xe_create(diff, PCMK__XE_DIFF_ADDED);
 
     crm_xml_add(diff, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
 
     tmp1 = subtract_v1_xml_object(removed, old, new, false, NULL,
                                   "removed:top");
     if (suppress && (tmp1 != NULL) && can_prune_leaf_v1(tmp1)) {
         pcmk__xml_free(tmp1);
     }
 
     tmp1 = subtract_v1_xml_object(added, new, old, true, NULL, "added:top");
     if (suppress && (tmp1 != NULL) && can_prune_leaf_v1(tmp1)) {
         pcmk__xml_free(tmp1);
     }
 
     if ((added->children == NULL) && (removed->children == NULL)) {
         pcmk__xml_free(diff);
         diff = NULL;
     }
 
     return diff;
 }
-
-// Deprecated functions kept only for backward API compatibility
-// LCOV_EXCL_START
-
-#include <crm/common/xml_compat.h>
-
-gboolean
-apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml)
-{
-    gboolean result = TRUE;
-    int root_nodes_seen = 0;
-    const char *digest = crm_element_value(diff, PCMK__XA_DIGEST);
-    const char *version = crm_element_value(diff, PCMK_XA_CRM_FEATURE_SET);
-
-    xmlNode *child_diff = NULL;
-    xmlNode *added = pcmk__xe_first_child(diff, PCMK__XE_DIFF_ADDED, NULL,
-                                          NULL);
-    xmlNode *removed = pcmk__xe_first_child(diff, PCMK__XE_DIFF_REMOVED, NULL,
-                                            NULL);
-
-    CRM_CHECK(new_xml != NULL, return FALSE);
-
-    crm_trace("Subtraction Phase");
-    for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL;
-         child_diff = pcmk__xml_next(child_diff)) {
-        CRM_CHECK(root_nodes_seen == 0, result = FALSE);
-        if (root_nodes_seen == 0) {
-            *new_xml = subtract_v1_xml_object(NULL, old_xml, child_diff, false,
-                                              NULL, NULL);
-        }
-        root_nodes_seen++;
-    }
-
-    if (root_nodes_seen == 0) {
-        *new_xml = pcmk__xml_copy(NULL, old_xml);
-
-    } else if (root_nodes_seen > 1) {
-        crm_err("(-) Diffs cannot contain more than one change set... saw %d",
-                root_nodes_seen);
-        result = FALSE;
-    }
-
-    root_nodes_seen = 0;
-    crm_trace("Addition Phase");
-    if (result) {
-        xmlNode *child_diff = NULL;
-
-        for (child_diff = pcmk__xml_first_child(added); child_diff != NULL;
-             child_diff = pcmk__xml_next(child_diff)) {
-            CRM_CHECK(root_nodes_seen == 0, result = FALSE);
-            if (root_nodes_seen == 0) {
-                pcmk__xml_update(NULL, *new_xml, child_diff, pcmk__xaf_none,
-                                 true);
-            }
-            root_nodes_seen++;
-        }
-    }
-
-    if (root_nodes_seen > 1) {
-        crm_err("(+) Diffs cannot contain more than one change set... saw %d",
-                root_nodes_seen);
-        result = FALSE;
-
-    } else if (result && (digest != NULL)) {
-        char *new_digest = NULL;
-
-        purge_v1_diff_markers(*new_xml);    // Purge now so diff is ok
-        new_digest = calculate_xml_versioned_digest(*new_xml, FALSE, TRUE,
-                                                    version);
-        if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) {
-            crm_info("Digest mis-match: expected %s, calculated %s",
-                     digest, new_digest);
-            result = FALSE;
-
-            pcmk__if_tracing(
-                {
-                    save_xml_to_file(old_xml, "diff:original", NULL);
-                    save_xml_to_file(diff, "diff:input", NULL);
-                    save_xml_to_file(*new_xml, "diff:new", NULL);
-                },
-                {}
-            );
-
-        } else {
-            crm_trace("Digest matched: expected %s, calculated %s",
-                      digest, new_digest);
-        }
-        free(new_digest);
-
-    } else if (result) {
-        purge_v1_diff_markers(*new_xml);    // Purge now so diff is ok
-    }
-
-    return result;
-}
-
-void
-purge_diff_markers(xmlNode *a_node)
-{
-    purge_v1_diff_markers(a_node);
-}
-
-xmlNode *
-diff_xml_object(xmlNode *old, xmlNode *new, gboolean suppress)
-{
-    return pcmk__diff_v1_xml_object(old, new, suppress);
-}
-
-xmlNode *
-subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right,
-                    gboolean full, gboolean *changed, const char *marker)
-{
-    return subtract_v1_xml_object(parent, left, right, full, changed, marker);
-}
-
-gboolean
-can_prune_leaf(xmlNode *xml_node)
-{
-    return can_prune_leaf_v1(xml_node);
-}
-
-// LCOV_EXCL_STOP
-// End deprecated API
diff --git a/lib/common/patchset_display.c b/lib/common/patchset_display.c
index 1351c86071..0855c6e829 100644
--- a/lib/common/patchset_display.c
+++ b/lib/common/patchset_display.c
@@ -1,530 +1,452 @@
 /*
  * 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 <crm_internal.h>
 
 #include <crm/common/xml.h>
 
 #include "crmcommon_private.h"
 
 /*!
  * \internal
  * \brief Output an XML patchset header
  *
  * This function parses a header from an XML patchset (an \p XML_ATTR_DIFF
  * element and its children).
  *
  * All header lines contain three integers separated by dots, of the form
  * <tt>{0}.{1}.{2}</tt>:
  * * \p {0}: \c PCMK_XA_ADMIN_EPOCH
  * * \p {1}: \c PCMK_XA_EPOCH
  * * \p {2}: \c PCMK_XA_NUM_UPDATES
  *
  * Lines containing \p "---" describe removals and end with the patch format
  * number. Lines containing \p "+++" describe additions and end with the patch
  * digest.
  *
  * \param[in,out] out       Output object
  * \param[in]     patchset  XML patchset to output
  *
  * \return Standard Pacemaker return code
  *
  * \note This function produces output only for text-like formats.
  */
 static int
 xml_show_patchset_header(pcmk__output_t *out, const xmlNode *patchset)
 {
     int rc = pcmk_rc_no_output;
     int add[] = { 0, 0, 0 };
     int del[] = { 0, 0, 0 };
 
     xml_patch_versions(patchset, add, del);
 
     if ((add[0] != del[0]) || (add[1] != del[1]) || (add[2] != del[2])) {
         const char *fmt = crm_element_value(patchset, PCMK_XA_FORMAT);
         const char *digest = crm_element_value(patchset, PCMK__XA_DIGEST);
 
         out->info(out, "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
         rc = out->info(out, "Diff: +++ %d.%d.%d %s",
                        add[0], add[1], add[2], digest);
 
     } else if ((add[0] != 0) || (add[1] != 0) || (add[2] != 0)) {
         rc = out->info(out, "Local-only Change: %d.%d.%d",
                        add[0], add[1], add[2]);
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Output a user-friendly form of XML additions or removals
  *
  * \param[in,out] out      Output object
  * \param[in]     prefix   String to prepend to every line of output
  * \param[in]     data     XML node to output
  * \param[in]     depth    Current indentation level
  * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
  *
  * \return Standard Pacemaker return code
  *
  * \note This function produces output only for text-like formats.
  */
 static int
 xml_show_patchset_v1_recursive(pcmk__output_t *out, const char *prefix,
                                const xmlNode *data, int depth, uint32_t options)
 {
     if ((data->children == NULL)
         || (crm_element_value(data, PCMK__XA_CRM_DIFF_MARKER) != NULL)) {
 
         // Found a change; clear the pcmk__xml_fmt_diff_short option if set
         options &= ~pcmk__xml_fmt_diff_short;
 
         if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) {
             prefix = PCMK__XML_PREFIX_CREATED;
         } else {    // pcmk_is_set(options, pcmk__xml_fmt_diff_minus)
             prefix = PCMK__XML_PREFIX_DELETED;
         }
     }
 
     if (pcmk_is_set(options, pcmk__xml_fmt_diff_short)) {
         int rc = pcmk_rc_no_output;
 
         // Keep looking for the actual change
         for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL;
              child = pcmk__xml_next(child)) {
             int temp_rc = xml_show_patchset_v1_recursive(out, prefix, child,
                                                          depth + 1, options);
             rc = pcmk__output_select_rc(rc, temp_rc);
         }
         return rc;
     }
 
     return pcmk__xml_show(out, prefix, data, depth,
                           options
                           |pcmk__xml_fmt_open
                           |pcmk__xml_fmt_children
                           |pcmk__xml_fmt_close);
 }
 
 /*!
  * \internal
  * \brief Output a user-friendly form of an XML patchset (format 1)
  *
  * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
  * children) into a user-friendly combined diff output.
  *
  * \param[in,out] out       Output object
  * \param[in]     patchset  XML patchset to output
  * \param[in]     options   Group of \p pcmk__xml_fmt_options flags
  *
  * \return Standard Pacemaker return code
  *
  * \note This function produces output only for text-like formats.
  */
 static int
 xml_show_patchset_v1(pcmk__output_t *out, const xmlNode *patchset,
                      uint32_t options)
 {
     const xmlNode *removed = NULL;
     const xmlNode *added = NULL;
     const xmlNode *child = NULL;
     bool is_first = true;
     int rc = xml_show_patchset_header(out, patchset);
 
     /* It's not clear whether "- " or "+ " ever does *not* get overridden by
      * PCMK__XML_PREFIX_DELETED or PCMK__XML_PREFIX_CREATED in practice.
      * However, v1 patchsets can only exist during rolling upgrades from
      * Pacemaker 1.1.11, so not worth worrying about.
      */
     removed = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_REMOVED, NULL, NULL);
     for (child = pcmk__xml_first_child(removed); child != NULL;
          child = pcmk__xml_next(child)) {
         int temp_rc = xml_show_patchset_v1_recursive(out, "- ", child, 0,
                                                      options
                                                      |pcmk__xml_fmt_diff_minus);
         rc = pcmk__output_select_rc(rc, temp_rc);
 
         if (is_first) {
             is_first = false;
         } else {
             rc = pcmk__output_select_rc(rc, out->info(out, " --- "));
         }
     }
 
     is_first = true;
     added = pcmk__xe_first_child(patchset, PCMK__XE_DIFF_ADDED, NULL, NULL);
     for (child = pcmk__xml_first_child(added); child != NULL;
          child = pcmk__xml_next(child)) {
         int temp_rc = xml_show_patchset_v1_recursive(out, "+ ", child, 0,
                                                      options
                                                      |pcmk__xml_fmt_diff_plus);
         rc = pcmk__output_select_rc(rc, temp_rc);
 
         if (is_first) {
             is_first = false;
         } else {
             rc = pcmk__output_select_rc(rc, out->info(out, " +++ "));
         }
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Output a user-friendly form of an XML patchset (format 2)
  *
  * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
  * children) into a user-friendly combined diff output.
  *
  * \param[in,out] out       Output object
  * \param[in]     patchset  XML patchset to output
  *
  * \return Standard Pacemaker return code
  *
  * \note This function produces output only for text-like formats.
  */
 static int
 xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset)
 {
     int rc = xml_show_patchset_header(out, patchset);
     int temp_rc = pcmk_rc_no_output;
 
     for (const xmlNode *change = pcmk__xe_first_child(patchset, NULL, NULL,
                                                       NULL);
          change != NULL; change = pcmk__xe_next(change)) {
 
         const char *op = crm_element_value(change, PCMK_XA_OPERATION);
         const char *xpath = crm_element_value(change, PCMK_XA_PATH);
 
         if (op == NULL) {
             continue;
         }
 
         if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
             char *prefix = crm_strdup_printf(PCMK__XML_PREFIX_CREATED " %s: ",
                                              xpath);
 
             temp_rc = pcmk__xml_show(out, prefix, change->children, 0,
                                      pcmk__xml_fmt_pretty|pcmk__xml_fmt_open);
             rc = pcmk__output_select_rc(rc, temp_rc);
 
             // Overwrite all except the first two characters with spaces
             for (char *ch = prefix + 2; *ch != '\0'; ch++) {
                 *ch = ' ';
             }
 
             temp_rc = pcmk__xml_show(out, prefix, change->children, 0,
                                      pcmk__xml_fmt_pretty
                                      |pcmk__xml_fmt_children
                                      |pcmk__xml_fmt_close);
             rc = pcmk__output_select_rc(rc, temp_rc);
             free(prefix);
 
         } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
             const char *position = crm_element_value(change, PCMK_XE_POSITION);
 
             temp_rc = out->info(out,
                                 PCMK__XML_PREFIX_MOVED " %s moved to offset %s",
                                 xpath, position);
             rc = pcmk__output_select_rc(rc, temp_rc);
 
         } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
             xmlNode *clist = pcmk__xe_first_child(change, PCMK_XE_CHANGE_LIST,
                                                   NULL, NULL);
             GString *buffer_set = NULL;
             GString *buffer_unset = NULL;
 
             for (const xmlNode *child = pcmk__xe_first_child(clist, NULL, NULL,
                                                              NULL);
                  child != NULL; child = pcmk__xe_next(child)) {
 
                 const char *name = crm_element_value(child, PCMK_XA_NAME);
 
                 op = crm_element_value(child, PCMK_XA_OPERATION);
                 if (op == NULL) {
                     continue;
                 }
 
                 if (strcmp(op, "set") == 0) {
                     const char *value = crm_element_value(child, PCMK_XA_VALUE);
 
                     pcmk__add_separated_word(&buffer_set, 256, "@", ", ");
                     pcmk__g_strcat(buffer_set, name, "=", value, NULL);
 
                 } else if (strcmp(op, "unset") == 0) {
                     pcmk__add_separated_word(&buffer_unset, 256, "@", ", ");
                     g_string_append(buffer_unset, name);
                 }
             }
 
             if (buffer_set != NULL) {
                 temp_rc = out->info(out, "+  %s:  %s", xpath, buffer_set->str);
                 rc = pcmk__output_select_rc(rc, temp_rc);
                 g_string_free(buffer_set, TRUE);
             }
 
             if (buffer_unset != NULL) {
                 temp_rc = out->info(out, "-- %s:  %s",
                                     xpath, buffer_unset->str);
                 rc = pcmk__output_select_rc(rc, temp_rc);
                 g_string_free(buffer_unset, TRUE);
             }
 
         } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
             int position = -1;
 
             crm_element_value_int(change, PCMK_XE_POSITION, &position);
             if (position >= 0) {
                 temp_rc = out->info(out, "-- %s (%d)", xpath, position);
             } else {
                 temp_rc = out->info(out, "-- %s", xpath);
             }
             rc = pcmk__output_select_rc(rc, temp_rc);
         }
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Output a user-friendly form of an XML patchset
  *
  * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
  * children) into a user-friendly combined diff output.
  *
  * \param[in,out] out   Output object
  * \param[in]     args  Message-specific arguments
  *
  * \return Standard Pacemaker return code
  *
  * \note \p args should contain the following:
  *       -# XML patchset
  */
 PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
 static int
 xml_patchset_default(pcmk__output_t *out, va_list args)
 {
     const xmlNode *patchset = va_arg(args, const xmlNode *);
 
     int format = 1;
 
     if (patchset == NULL) {
         crm_trace("Empty patch");
         return pcmk_rc_no_output;
     }
 
     crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
     switch (format) {
         case 1:
             return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty);
         case 2:
             return xml_show_patchset_v2(out, patchset);
         default:
             crm_err("Unknown patch format: %d", format);
             return pcmk_rc_bad_xml_patch;
     }
 }
 
 /*!
  * \internal
  * \brief Output a user-friendly form of an XML patchset
  *
  * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its
  * children) into a user-friendly combined diff output.
  *
  * \param[in,out] out   Output object
  * \param[in]     args  Message-specific arguments
  *
  * \return Standard Pacemaker return code
  *
  * \note \p args should contain the following:
  *       -# XML patchset
  */
 PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
 static int
 xml_patchset_log(pcmk__output_t *out, va_list args)
 {
     static struct qb_log_callsite *patchset_cs = NULL;
 
     const xmlNode *patchset = va_arg(args, const xmlNode *);
 
     uint8_t log_level = pcmk__output_get_log_level(out);
     int format = 1;
 
     if (log_level == LOG_NEVER) {
         return pcmk_rc_no_output;
     }
 
     if (patchset == NULL) {
         crm_trace("Empty patch");
         return pcmk_rc_no_output;
     }
 
     if (patchset_cs == NULL) {
         patchset_cs = qb_log_callsite_get(__func__, __FILE__, "xml-patchset",
                                           log_level, __LINE__,
                                           crm_trace_nonlog);
     }
 
     if (!crm_is_callsite_active(patchset_cs, log_level, crm_trace_nonlog)) {
         // Nothing would be logged, so skip all the work
         return pcmk_rc_no_output;
     }
 
     crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
     switch (format) {
         case 1:
             if (log_level < LOG_DEBUG) {
                 return xml_show_patchset_v1(out, patchset,
                                             pcmk__xml_fmt_pretty
                                             |pcmk__xml_fmt_diff_short);
             }
             return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty);
         case 2:
             return xml_show_patchset_v2(out, patchset);
         default:
             crm_err("Unknown patch format: %d", format);
             return pcmk_rc_bad_xml_patch;
     }
 }
 
 /*!
  * \internal
  * \brief Output an XML patchset
  *
  * This function outputs an XML patchset (an \p XML_ATTR_DIFF element and its
  * children) without modification, as a CDATA block.
  *
  * \param[in,out] out   Output object
  * \param[in]     args  Message-specific arguments
  *
  * \return Standard Pacemaker return code
  *
  * \note \p args should contain the following:
  *       -# XML patchset
  */
 PCMK__OUTPUT_ARGS("xml-patchset", "const xmlNode *")
 static int
 xml_patchset_xml(pcmk__output_t *out, va_list args)
 {
     const xmlNode *patchset = va_arg(args, const xmlNode *);
 
     if (patchset != NULL) {
         GString *buf = g_string_sized_new(1024);
 
         pcmk__xml_string(patchset, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
                          0);
 
         out->output_xml(out, PCMK_XE_XML_PATCHSET, buf->str);
         g_string_free(buf, TRUE);
         return pcmk_rc_ok;
     }
     crm_trace("Empty patch");
     return pcmk_rc_no_output;
 }
 
 static pcmk__message_entry_t fmt_functions[] = {
     { "xml-patchset", "default", xml_patchset_default },
     { "xml-patchset", "log", xml_patchset_log },
     { "xml-patchset", "xml", xml_patchset_xml },
 
     { NULL, NULL, NULL }
 };
 
 /*!
  * \internal
  * \brief Register the formatting functions for XML patchsets
  *
  * \param[in,out] out  Output object
  */
 void
 pcmk__register_patchset_messages(pcmk__output_t *out) {
     pcmk__register_messages(out, fmt_functions);
 }
-
-// Deprecated functions kept only for backward API compatibility
-// LCOV_EXCL_START
-
-#include <crm/common/xml_compat.h>
-
-void
-xml_log_patchset(uint8_t log_level, const char *function,
-                 const xmlNode *patchset)
-{
-    /* This function has some duplication relative to the message functions.
-     * This way, we can maintain the const xmlNode * in the signature. The
-     * message functions must be non-const. They have to support XML output
-     * objects, which must make a copy of a the patchset, requiring a non-const
-     * function call.
-     *
-     * In contrast, this legacy function doesn't need to support XML output.
-     */
-    static struct qb_log_callsite *patchset_cs = NULL;
-
-    pcmk__output_t *out = NULL;
-    int format = 1;
-    int rc = pcmk_rc_no_output;
-
-    switch (log_level) {
-        case LOG_NEVER:
-            return;
-        case LOG_STDOUT:
-            CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return);
-            break;
-        default:
-            if (patchset_cs == NULL) {
-                patchset_cs = qb_log_callsite_get(__func__, __FILE__,
-                                                  "xml-patchset", log_level,
-                                                  __LINE__, crm_trace_nonlog);
-            }
-            if (!crm_is_callsite_active(patchset_cs, log_level,
-                                        crm_trace_nonlog)) {
-                return;
-            }
-            CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return);
-            pcmk__output_set_log_level(out, log_level);
-            break;
-    }
-
-    if (patchset == NULL) {
-        // Should come after the LOG_NEVER check
-        crm_trace("Empty patch");
-        goto done;
-    }
-
-    crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
-    switch (format) {
-        case 1:
-            if (log_level < LOG_DEBUG) {
-                rc = xml_show_patchset_v1(out, patchset,
-                                          pcmk__xml_fmt_pretty
-                                          |pcmk__xml_fmt_diff_short);
-            } else {    // Note: LOG_STDOUT > LOG_DEBUG
-                rc = xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty);
-            }
-            break;
-        case 2:
-            rc = xml_show_patchset_v2(out, patchset);
-            break;
-        default:
-            crm_err("Unknown patch format: %d", format);
-            rc = pcmk_rc_bad_xml_patch;
-            break;
-    }
-
-done:
-    out->finish(out, pcmk_rc2exitc(rc), true, NULL);
-    pcmk__output_free(out);
-}
-
-// LCOV_EXCL_STOP
-// End deprecated API
diff --git a/lib/common/schemas.c b/lib/common/schemas.c
index 65345c46b3..a123085048 100644
--- a/lib/common/schemas.c
+++ b/lib/common/schemas.c
@@ -1,1744 +1,1621 @@
 /*
  * 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 <crm_internal.h>
 
 #include <stdio.h>
 #include <string.h>
 #include <dirent.h>
 #include <errno.h>
 #include <sys/stat.h>
 #include <stdarg.h>
 
 #include <libxml/relaxng.h>
 #include <libxslt/xslt.h>
 #include <libxslt/transform.h>
 #include <libxslt/security.h>
 #include <libxslt/xsltutils.h>
 
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>  /* PCMK__XML_LOG_BASE */
 
 #include "crmcommon_private.h"
 
 #define SCHEMA_ZERO { .v = { 0, 0 } }
 
 #define schema_strdup_printf(prefix, version, suffix) \
     crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1])
 
 typedef struct {
     xmlRelaxNGPtr rng;
     xmlRelaxNGValidCtxtPtr valid;
     xmlRelaxNGParserCtxtPtr parser;
 } relaxng_ctx_cache_t;
 
 static GList *known_schemas = NULL;
 static bool initialized = false;
 static bool silent_logging = FALSE;
 
 static void G_GNUC_PRINTF(2, 3)
 xml_log(int priority, const char *fmt, ...)
 {
     va_list ap;
 
     va_start(ap, fmt);
     if (silent_logging == FALSE) {
         /* XXX should not this enable dechunking as well? */
         PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap);
     }
     va_end(ap);
 }
 
 static int
 xml_latest_schema_index(void)
 {
     /* This function assumes that pcmk__schema_init() has been called
      * beforehand, so we have at least three schemas (one real schema, the
      * "pacemaker-next" schema, and the "none" schema).
      *
      * @COMPAT: pacemaker-next is deprecated since 2.1.5 and none since 2.1.8.
      * Update this when we drop those.
      */
     return g_list_length(known_schemas) - 3;
 }
 
 /*!
  * \internal
  * \brief Return the schema entry of the highest-versioned schema
  *
  * \return Schema entry of highest-versioned schema (or NULL on error)
  */
 static GList *
 get_highest_schema(void)
 {
     /* The highest numerically versioned schema is the one before pacemaker-next
      *
      * @COMPAT pacemaker-next is deprecated since 2.1.5
      */
     GList *entry = pcmk__get_schema("pacemaker-next");
 
     CRM_ASSERT((entry != NULL) && (entry->prev != NULL));
     return entry->prev;
 }
 
 /*!
  * \internal
  * \brief Return the name of the highest-versioned schema
  *
  * \return Name of highest-versioned schema (or NULL on error)
  */
 const char *
 pcmk__highest_schema_name(void)
 {
     GList *entry = get_highest_schema();
 
     return ((pcmk__schema_t *)(entry->data))->name;
 }
 
 /*!
  * \internal
  * \brief Find first entry of highest major schema version series
  *
  * \return Schema entry of first schema with highest major version
  */
 GList *
 pcmk__find_x_0_schema(void)
 {
 #if defined(PCMK__UNIT_TESTING)
     /* If we're unit testing, this can't be static because it'll stick
      * around from one test run to the next. It needs to be cleared out
      * every time.
      */
     GList *x_0_entry = NULL;
 #else
     static GList *x_0_entry = NULL;
 #endif
 
     pcmk__schema_t *highest_schema = NULL;
 
     if (x_0_entry != NULL) {
         return x_0_entry;
     }
     x_0_entry = get_highest_schema();
     highest_schema = x_0_entry->data;
 
     for (GList *iter = x_0_entry->prev; iter != NULL; iter = iter->prev) {
         pcmk__schema_t *schema = iter->data;
 
         /* We've found a schema in an older major version series.  Return
          * the index of the first one in the same major version series as
          * the highest schema.
          */
         if (schema->version.v[0] < highest_schema->version.v[0]) {
             x_0_entry = iter->next;
             break;
         }
 
         /* We're out of list to examine.  This probably means there was only
          * one major version series, so return the first schema entry.
          */
         if (iter->prev == NULL) {
             x_0_entry = known_schemas->data;
             break;
         }
     }
     return x_0_entry;
 }
 
 static inline bool
 version_from_filename(const char *filename, pcmk__schema_version_t *version)
 {
     if (pcmk__ends_with(filename, ".rng")) {
         return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2;
     } else {
         return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2;
     }
 }
 
 static int
 schema_filter(const struct dirent *a)
 {
     int rc = 0;
     pcmk__schema_version_t version = SCHEMA_ZERO;
 
     if (strstr(a->d_name, "pacemaker-") != a->d_name) {
         /* crm_trace("%s - wrong prefix", a->d_name); */
 
     } else if (!pcmk__ends_with_ext(a->d_name, ".rng")) {
         /* crm_trace("%s - wrong suffix", a->d_name); */
 
     } else if (!version_from_filename(a->d_name, &version)) {
         /* crm_trace("%s - wrong format", a->d_name); */
 
     } else {
         /* crm_debug("%s - candidate", a->d_name); */
         rc = 1;
     }
 
     return rc;
 }
 
 static int
 schema_cmp(pcmk__schema_version_t a_version, pcmk__schema_version_t b_version)
 {
     for (int i = 0; i < 2; ++i) {
         if (a_version.v[i] < b_version.v[i]) {
             return -1;
         } else if (a_version.v[i] > b_version.v[i]) {
             return 1;
         }
     }
     return 0;
 }
 
 static int
 schema_cmp_directory(const struct dirent **a, const struct dirent **b)
 {
     pcmk__schema_version_t a_version = SCHEMA_ZERO;
     pcmk__schema_version_t b_version = SCHEMA_ZERO;
 
     if (!version_from_filename(a[0]->d_name, &a_version)
         || !version_from_filename(b[0]->d_name, &b_version)) {
         // Shouldn't be possible, but makes static analysis happy
         return 0;
     }
 
     return schema_cmp(a_version, b_version);
 }
 
 /*!
  * \internal
  * \brief Add given schema + auxiliary data to internal bookkeeping.
  *
  * \note When providing \p version, should not be called directly but
  *       through \c add_schema_by_version.
  */
 static void
 add_schema(enum pcmk__schema_validator validator, const pcmk__schema_version_t *version,
            const char *name, const char *transform,
            const char *transform_enter, bool transform_onleave)
 {
     pcmk__schema_t *schema = NULL;
 
     schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t));
 
     schema->validator = validator;
     schema->version.v[0] = version->v[0];
     schema->version.v[1] = version->v[1];
     schema->transform_onleave = transform_onleave;
     // schema->schema_index is set after all schemas are loaded and sorted
 
     if (version->v[0] || version->v[1]) {
         schema->name = schema_strdup_printf("pacemaker-", *version, "");
     } else {
         schema->name = pcmk__str_copy(name);
     }
 
     if (transform) {
         schema->transform = pcmk__str_copy(transform);
     }
 
     if (transform_enter) {
         schema->transform_enter = pcmk__str_copy(transform_enter);
     }
 
     known_schemas = g_list_prepend(known_schemas, schema);
 }
 
 /*!
  * \internal
  * \brief Add version-specified schema + auxiliary data to internal bookkeeping.
  * \return Standard Pacemaker return value (the only possible values are
  * \c ENOENT when no upgrade schema is associated, or \c pcmk_rc_ok otherwise.
  *
  * \note There's no reliance on the particular order of schemas entering here.
  *
  * \par A bit of theory
  * We track 3 XSLT stylesheets that differ per usage:
  * - "upgrade":
  *   . sparsely spread over the sequence of all available schemas,
  *     as they are only relevant when major version of the schema
  *     is getting bumped -- in that case, it MUST be set
  *   . name convention:  upgrade-X.Y.xsl
  * - "upgrade-enter":
  *   . may only accompany "upgrade" occurrence, but doesn't need to
  *     be present anytime such one is, i.e., it MAY not be set when
  *     "upgrade" is
  *   . name convention:  upgrade-X.Y-enter.xsl,
  *     when not present: upgrade-enter.xsl
  * - "upgrade-leave":
  *   . like "upgrade-enter", but SHOULD be present whenever
  *     "upgrade-enter" is (and vice versa, but that's only
  *     to prevent confusion based on observing the files,
  *     it would get ignored regardless)
  *   . name convention:  (see "upgrade-enter")
  */
 static int
 add_schema_by_version(const pcmk__schema_version_t *version, bool transform_expected)
 {
     bool transform_onleave = FALSE;
     int rc = pcmk_rc_ok;
     struct stat s;
     char *xslt = NULL,
          *transform_upgrade = NULL,
          *transform_enter = NULL;
 
     /* prologue for further transform_expected handling */
     if (transform_expected) {
         /* check if there's suitable "upgrade" stylesheet */
         transform_upgrade = schema_strdup_printf("upgrade-", *version, );
         xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
                                        transform_upgrade);
     }
 
     if (!transform_expected) {
         /* jump directly to the end */
 
     } else if (stat(xslt, &s) == 0) {
         /* perhaps there's also a targeted "upgrade-enter" stylesheet */
         transform_enter = schema_strdup_printf("upgrade-", *version, "-enter");
         free(xslt);
         xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
                                        transform_enter);
         if (stat(xslt, &s) != 0) {
             /* or initially, at least a generic one */
             crm_debug("Upgrade-enter transform %s.xsl not found", xslt);
             free(xslt);
             free(transform_enter);
             transform_enter = strdup("upgrade-enter");
             xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
                                            transform_enter);
             if (stat(xslt, &s) != 0) {
                 crm_debug("Upgrade-enter transform %s.xsl not found, either", xslt);
                 free(xslt);
                 xslt = NULL;
             }
         }
         /* xslt contains full path to "upgrade-enter" stylesheet */
         if (xslt != NULL) {
             /* then there should be "upgrade-leave" counterpart (enter->leave) */
             memcpy(strrchr(xslt, '-') + 1, "leave", sizeof("leave") - 1);
             transform_onleave = (stat(xslt, &s) == 0);
             free(xslt);
         } else {
             free(transform_enter);
             transform_enter = NULL;
         }
 
     } else {
         crm_err("Upgrade transform %s not found", xslt);
         free(xslt);
         free(transform_upgrade);
         transform_upgrade = NULL;
         rc = ENOENT;
     }
 
     add_schema(pcmk__schema_validator_rng, version, NULL,
                transform_upgrade, transform_enter, transform_onleave);
 
     free(transform_upgrade);
     free(transform_enter);
 
     return rc;
 }
 
 static void
 wrap_libxslt(bool finalize)
 {
     static xsltSecurityPrefsPtr secprefs;
     int ret = 0;
 
     /* security framework preferences */
     if (!finalize) {
         CRM_ASSERT(secprefs == NULL);
         secprefs = xsltNewSecurityPrefs();
         ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE,
                                    xsltSecurityForbid)
               | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY,
                                      xsltSecurityForbid)
               | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK,
                                      xsltSecurityForbid)
               | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK,
                                      xsltSecurityForbid);
         if (ret != 0) {
             return;
         }
     } else {
         xsltFreeSecurityPrefs(secprefs);
         secprefs = NULL;
     }
 
     /* cleanup only */
     if (finalize) {
         xsltCleanupGlobals();
     }
 }
 
 void
 pcmk__load_schemas_from_dir(const char *dir)
 {
     int lpc, max;
     struct dirent **namelist = NULL;
 
     max = scandir(dir, &namelist, schema_filter, schema_cmp_directory);
     if (max < 0) {
         crm_warn("Could not load schemas from %s: %s", dir, strerror(errno));
         return;
     }
 
     for (lpc = 0; lpc < max; lpc++) {
         bool transform_expected = false;
         pcmk__schema_version_t version = SCHEMA_ZERO;
 
         if (!version_from_filename(namelist[lpc]->d_name, &version)) {
             // Shouldn't be possible, but makes static analysis happy
             crm_warn("Skipping schema '%s': could not parse version",
                      namelist[lpc]->d_name);
             continue;
         }
         if ((lpc + 1) < max) {
             pcmk__schema_version_t next_version = SCHEMA_ZERO;
 
             if (version_from_filename(namelist[lpc+1]->d_name, &next_version)
                     && (version.v[0] < next_version.v[0])) {
                 transform_expected = true;
             }
         }
 
         if (add_schema_by_version(&version, transform_expected) != pcmk_rc_ok) {
             break;
         }
     }
 
     for (lpc = 0; lpc < max; lpc++) {
         free(namelist[lpc]);
     }
 
     free(namelist);
 }
 
 static gint
 schema_sort_GCompareFunc(gconstpointer a, gconstpointer b)
 {
     const pcmk__schema_t *schema_a = a;
     const pcmk__schema_t *schema_b = b;
 
     // @COMPAT pacemaker-next is deprecated since 2.1.5 and none since 2.1.8
     if (pcmk__str_eq(schema_a->name, "pacemaker-next", pcmk__str_none)) {
         if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) {
             return -1;
         } else {
             return 1;
         }
     } else if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) {
         return 1;
     } else if (pcmk__str_eq(schema_b->name, "pacemaker-next", pcmk__str_none)) {
         return -1;
     } else {
         return schema_cmp(schema_a->version, schema_b->version);
     }
 }
 
 /*!
  * \internal
  * \brief Sort the list of known schemas such that all pacemaker-X.Y are in
  *        version order, then pacemaker-next, then none
  *
  * This function should be called whenever additional schemas are loaded using
  * \c pcmk__load_schemas_from_dir(), after the initial sets in
  * \c pcmk__schema_init().
  */
 void
 pcmk__sort_schemas(void)
 {
     known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc);
 }
 
 /*!
  * \internal
  * \brief Load pacemaker schemas into cache
  *
  * \note This currently also serves as an entry point for the
  *       generic initialization of the libxslt library.
  */
 void
 pcmk__schema_init(void)
 {
     if (!initialized) {
         const char *remote_schema_dir = pcmk__remote_schema_dir();
         char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng);
         const pcmk__schema_version_t zero = SCHEMA_ZERO;
         int schema_index = 0;
 
         initialized = true;
 
         wrap_libxslt(false);
 
         pcmk__load_schemas_from_dir(base);
         pcmk__load_schemas_from_dir(remote_schema_dir);
         free(base);
 
         // @COMPAT: Deprecated since 2.1.5
         add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", NULL,
                    NULL, FALSE);
 
         // @COMPAT Deprecated since 2.1.8
         add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE, NULL,
                    NULL, FALSE);
 
         /* add_schema() prepends items to the list, so in the simple case, this
          * just reverses the list. However if there were any remote schemas,
          * sorting is necessary.
          */
         pcmk__sort_schemas();
 
         // Now set the schema indexes and log the final result
         for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
             pcmk__schema_t *schema = iter->data;
 
             if (schema->transform == NULL) {
                 crm_debug("Loaded schema %d: %s", schema_index, schema->name);
             } else {
                 crm_debug("Loaded schema %d: %s (upgrades with %s.xsl)",
                           schema_index, schema->name, schema->transform);
             }
             schema->schema_index = schema_index++;
         }
     }
 }
 
 static bool
 validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler,
                       void *error_handler_context, const char *relaxng_file,
                       relaxng_ctx_cache_t **cached_ctx)
 {
     int rc = 0;
     bool valid = true;
     relaxng_ctx_cache_t *ctx = NULL;
 
     CRM_CHECK(doc != NULL, return false);
     CRM_CHECK(relaxng_file != NULL, return false);
 
     if (cached_ctx && *cached_ctx) {
         ctx = *cached_ctx;
 
     } else {
         crm_debug("Creating RNG parser context");
         ctx = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t));
 
         ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file);
         CRM_CHECK(ctx->parser != NULL, goto cleanup);
 
         if (error_handler) {
             xmlRelaxNGSetParserErrors(ctx->parser,
                                       (xmlRelaxNGValidityErrorFunc) error_handler,
                                       (xmlRelaxNGValidityWarningFunc) error_handler,
                                       error_handler_context);
         } else {
             xmlRelaxNGSetParserErrors(ctx->parser,
                                       (xmlRelaxNGValidityErrorFunc) fprintf,
                                       (xmlRelaxNGValidityWarningFunc) fprintf,
                                       stderr);
         }
 
         ctx->rng = xmlRelaxNGParse(ctx->parser);
         CRM_CHECK(ctx->rng != NULL,
                   crm_err("Could not find/parse %s", relaxng_file);
                   goto cleanup);
 
         ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng);
         CRM_CHECK(ctx->valid != NULL, goto cleanup);
 
         if (error_handler) {
             xmlRelaxNGSetValidErrors(ctx->valid,
                                      (xmlRelaxNGValidityErrorFunc) error_handler,
                                      (xmlRelaxNGValidityWarningFunc) error_handler,
                                      error_handler_context);
         } else {
             xmlRelaxNGSetValidErrors(ctx->valid,
                                      (xmlRelaxNGValidityErrorFunc) fprintf,
                                      (xmlRelaxNGValidityWarningFunc) fprintf,
                                      stderr);
         }
     }
 
     rc = xmlRelaxNGValidateDoc(ctx->valid, doc);
     if (rc > 0) {
         valid = false;
 
     } else if (rc < 0) {
         crm_err("Internal libxml error during validation");
     }
 
   cleanup:
 
     if (cached_ctx) {
         *cached_ctx = ctx;
 
     } else {
         if (ctx->parser != NULL) {
             xmlRelaxNGFreeParserCtxt(ctx->parser);
         }
         if (ctx->valid != NULL) {
             xmlRelaxNGFreeValidCtxt(ctx->valid);
         }
         if (ctx->rng != NULL) {
             xmlRelaxNGFree(ctx->rng);
         }
         free(ctx);
     }
 
     return valid;
 }
 
 static void
 free_schema(gpointer data)
 {
     pcmk__schema_t *schema = data;
     relaxng_ctx_cache_t *ctx = NULL;
 
     switch (schema->validator) {
         case pcmk__schema_validator_none: // not cached
             break;
 
         case pcmk__schema_validator_rng: // cached
             ctx = (relaxng_ctx_cache_t *) schema->cache;
             if (ctx == NULL) {
                 break;
             }
 
             if (ctx->parser != NULL) {
                 xmlRelaxNGFreeParserCtxt(ctx->parser);
             }
 
             if (ctx->valid != NULL) {
                 xmlRelaxNGFreeValidCtxt(ctx->valid);
             }
 
             if (ctx->rng != NULL) {
                 xmlRelaxNGFree(ctx->rng);
             }
 
             free(ctx);
             schema->cache = NULL;
             break;
     }
 
     free(schema->name);
     free(schema->transform);
     free(schema->transform_enter);
     free(schema);
 }
 
 /*!
  * \internal
  * \brief Clean up global memory associated with XML schemas
  */
 void
 pcmk__schema_cleanup(void)
 {
     if (known_schemas != NULL) {
         g_list_free_full(known_schemas, free_schema);
         known_schemas = NULL;
     }
     initialized = false;
 
     wrap_libxslt(true);
 }
 
 /*!
  * \internal
  * \brief Get schema list entry corresponding to a schema name
  *
  * \param[in] name  Name of schema to get
  *
  * \return Schema list entry corresponding to \p name, or NULL if unknown
  */
 GList *
 pcmk__get_schema(const char *name)
 {
     // @COMPAT Not specifying a schema name is deprecated since 2.1.8
     if (name == NULL) {
         name = PCMK_VALUE_NONE;
     }
     for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
         pcmk__schema_t *schema = iter->data;
 
         if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
             return iter;
         }
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Compare two schema version numbers given the schema names
  *
  * \param[in] schema1  Name of first schema to compare
  * \param[in] schema2  Name of second schema to compare
  *
  * \return Standard comparison result (negative integer if \p schema1 has the
  *         lower version number, positive integer if \p schema1 has the higher
  *         version number, of 0 if the version numbers are equal)
  */
 int
 pcmk__cmp_schemas_by_name(const char *schema1_name, const char *schema2_name)
 {
     GList *entry1 = pcmk__get_schema(schema1_name);
     GList *entry2 = pcmk__get_schema(schema2_name);
 
     if (entry1 == NULL) {
         return (entry2 == NULL)? 0 : -1;
 
     } else if (entry2 == NULL) {
         return 1;
 
     } else {
         pcmk__schema_t *schema1 = entry1->data;
         pcmk__schema_t *schema2 = entry2->data;
 
         return schema1->schema_index - schema2->schema_index;
     }
 }
 
 static bool
 validate_with(xmlNode *xml, pcmk__schema_t *schema,
               xmlRelaxNGValidityErrorFunc error_handler,
               void *error_handler_context)
 {
     bool valid = false;
     char *file = NULL;
     relaxng_ctx_cache_t **cache = NULL;
 
     if (schema == NULL) {
         return false;
     }
 
     if (schema->validator == pcmk__schema_validator_none) {
         return true;
     }
 
     file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng,
                                    schema->name);
 
     crm_trace("Validating with %s (type=%d)",
               pcmk__s(file, "missing schema"), schema->validator);
     switch (schema->validator) {
         case pcmk__schema_validator_rng:
             cache = (relaxng_ctx_cache_t **) &(schema->cache);
             valid = validate_with_relaxng(xml->doc, error_handler, error_handler_context, file, cache);
             break;
         default:
             crm_err("Unknown validator type: %d", schema->validator);
             break;
     }
 
     free(file);
     return valid;
 }
 
 static bool
 validate_with_silent(xmlNode *xml, pcmk__schema_t *schema)
 {
     bool rc, sl_backup = silent_logging;
     silent_logging = TRUE;
     rc = validate_with(xml, schema, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR));
     silent_logging = sl_backup;
     return rc;
 }
 
 bool
 pcmk__validate_xml(xmlNode *xml_blob, const char *validation,
                    xmlRelaxNGValidityErrorFunc error_handler,
                    void *error_handler_context)
 {
     GList *entry = NULL;
     pcmk__schema_t *schema = NULL;
 
     CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false);
 
     if (validation == NULL) {
         validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH);
     }
     pcmk__warn_if_schema_deprecated(validation);
 
     // @COMPAT Not specifying a schema name is deprecated since 2.1.8
     if (validation == NULL) {
         bool valid = false;
 
         for (entry = known_schemas; entry != NULL; entry = entry->next) {
             schema = entry->data;
             if (validate_with(xml_blob, schema, NULL, NULL)) {
                 valid = true;
                 crm_xml_add(xml_blob, PCMK_XA_VALIDATE_WITH, schema->name);
                 crm_info("XML validated against %s", schema->name);
             }
         }
         return valid;
     }
 
     entry = pcmk__get_schema(validation);
     if (entry == NULL) {
         pcmk__config_err("Cannot validate CIB with " PCMK_XA_VALIDATE_WITH
                          " set to an unknown schema such as '%s' (manually"
                          " edit to use a known schema)",
                          validation);
         return false;
     }
 
     schema = entry->data;
     return validate_with(xml_blob, schema, error_handler,
                          error_handler_context);
 }
 
 /*!
  * \internal
  * \brief Validate XML using its configured schema (and send errors to logs)
  *
  * \param[in] xml  XML to validate
  *
  * \return true if XML validates, otherwise false
  */
 bool
 pcmk__configured_schema_validates(xmlNode *xml)
 {
     return pcmk__validate_xml(xml, NULL,
                               (xmlRelaxNGValidityErrorFunc) xml_log,
                               GUINT_TO_POINTER(LOG_ERR));
 }
 
 /* With this arrangement, an attempt to identify the message severity
    as explicitly signalled directly from XSLT is performed in rather
    a smart way (no reliance on formatting string + arguments being
    always specified as ["%s", purposeful_string], as it can also be
    ["%s: %s", some_prefix, purposeful_string] etc. so every argument
    pertaining %s specifier is investigated), and if such a mark found,
    the respective level is determined and, when the messages are to go
    to the native logs, the mark itself gets dropped
    (by the means of string shift).
 
    NOTE: whether the native logging is the right sink is decided per
          the ctx parameter -- NULL denotes this case, otherwise it
          carries a pointer to the numeric expression of the desired
          target logging level (messages with higher level will be
          suppressed)
 
    NOTE: on some architectures, this string shift may not have any
          effect, but that's an acceptable tradeoff
 
    The logging level for not explicitly designated messages
    (suspicious, likely internal errors or some runaways) is
    LOG_WARNING.
  */
 static void G_GNUC_PRINTF(2, 3)
 cib_upgrade_err(void *ctx, const char *fmt, ...)
 {
     va_list ap, aq;
     char *arg_cur;
 
     bool found = FALSE;
     const char *fmt_iter = fmt;
     uint8_t msg_log_level = LOG_WARNING;  /* default for runaway messages */
     const unsigned * log_level = (const unsigned *) ctx;
     enum {
         escan_seennothing,
         escan_seenpercent,
     } scan_state = escan_seennothing;
 
     va_start(ap, fmt);
     va_copy(aq, ap);
 
     while (!found && *fmt_iter != '\0') {
         /* while casing schema borrowed from libqb:qb_vsnprintf_serialize */
         switch (*fmt_iter++) {
         case '%':
             if (scan_state == escan_seennothing) {
                 scan_state = escan_seenpercent;
             } else if (scan_state == escan_seenpercent) {
                 scan_state = escan_seennothing;
             }
             break;
         case 's':
             if (scan_state == escan_seenpercent) {
                 scan_state = escan_seennothing;
                 arg_cur = va_arg(aq, char *);
                 if (arg_cur != NULL) {
                     switch (arg_cur[0]) {
                     case 'W':
                         if (!strncmp(arg_cur, "WARNING: ",
                                      sizeof("WARNING: ") - 1)) {
                             msg_log_level = LOG_WARNING;
                         }
                         if (ctx == NULL) {
                             memmove(arg_cur, arg_cur + sizeof("WARNING: ") - 1,
                                     strlen(arg_cur + sizeof("WARNING: ") - 1) + 1);
                         }
                         found = TRUE;
                         break;
                     case 'I':
                         if (!strncmp(arg_cur, "INFO: ",
                                      sizeof("INFO: ") - 1)) {
                             msg_log_level = LOG_INFO;
                         }
                         if (ctx == NULL) {
                             memmove(arg_cur, arg_cur + sizeof("INFO: ") - 1,
                                     strlen(arg_cur + sizeof("INFO: ") - 1) + 1);
                         }
                         found = TRUE;
                         break;
                     case 'D':
                         if (!strncmp(arg_cur, "DEBUG: ",
                                      sizeof("DEBUG: ") - 1)) {
                             msg_log_level = LOG_DEBUG;
                         }
                         if (ctx == NULL) {
                             memmove(arg_cur, arg_cur + sizeof("DEBUG: ") - 1,
                                     strlen(arg_cur + sizeof("DEBUG: ") - 1) + 1);
                         }
                         found = TRUE;
                         break;
                     }
                 }
             }
             break;
         case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.':
         case '0': case '1': case '2': case '3': case '4':
         case '5': case '6': case '7': case '8': case '9':
         case '*':
             break;
         case 'l':
         case 'z':
         case 't':
         case 'j':
         case 'd': case 'i':
         case 'o':
         case 'u':
         case 'x': case 'X':
         case 'e': case 'E':
         case 'f': case 'F':
         case 'g': case 'G':
         case 'a': case 'A':
         case 'c':
         case 'p':
             if (scan_state == escan_seenpercent) {
                 (void) va_arg(aq, void *);  /* skip forward */
                 scan_state = escan_seennothing;
             }
             break;
         default:
             scan_state = escan_seennothing;
             break;
         }
     }
 
     if (log_level != NULL) {
         /* intention of the following offset is:
            cibadmin -V -> start showing INFO labelled messages */
         if (*log_level + 4 >= msg_log_level) {
             vfprintf(stderr, fmt, ap);
         }
     } else {
         PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap);
     }
 
     va_end(aq);
     va_end(ap);
 }
 
 /*!
  * \internal
  * \brief Apply a single XSL transformation to given XML
  *
  * \param[in] xml        XML to transform
  * \param[in] transform  XSL name
  * \param[in] to_logs    If false, certain validation errors will be sent to
  *                       stderr rather than logged
  *
  * \return Transformed XML on success, otherwise NULL
  */
 static xmlNode *
 apply_transformation(const xmlNode *xml, const char *transform,
                      gboolean to_logs)
 {
     char *xform = NULL;
     xmlNode *out = NULL;
     xmlDocPtr res = NULL;
     xsltStylesheet *xslt = NULL;
 
     xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
                                     transform);
 
     /* for capturing, e.g., what's emitted via <xsl:message> */
     if (to_logs) {
         xsltSetGenericErrorFunc(NULL, cib_upgrade_err);
     } else {
         xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err);
     }
 
     xslt = xsltParseStylesheetFile((pcmkXmlStr) xform);
     CRM_CHECK(xslt != NULL, goto cleanup);
 
     res = xsltApplyStylesheet(xslt, xml->doc, NULL);
     CRM_CHECK(res != NULL, goto cleanup);
 
     xsltSetGenericErrorFunc(NULL, NULL);  /* restore default one */
 
     out = xmlDocGetRootElement(res);
 
   cleanup:
     if (xslt) {
         xsltFreeStylesheet(xslt);
     }
 
     free(xform);
 
     return out;
 }
 
 /*!
  * \internal
  * \brief Perform all transformations needed to upgrade XML to next schema
  *
  * A schema upgrade can require up to three XSL transformations: an "enter"
  * transform, the main upgrade transform, and a "leave" transform. Perform
  * all needed transforms to upgrade given XML to the next schema.
  *
  * \param[in] original_xml  XML to transform
  * \param[in] schema_index  Index of schema that successfully validates
  *                          \p original_xml
  * \param[in] to_logs       If false, certain validation errors will be sent to
  *                          stderr rather than logged
  *
  * \return XML result of schema transforms if successful, otherwise NULL
  */
 static xmlNode *
 apply_upgrade(const xmlNode *original_xml, int schema_index, gboolean to_logs)
 {
     pcmk__schema_t *schema = g_list_nth_data(known_schemas, schema_index);
     pcmk__schema_t *upgraded_schema = g_list_nth_data(known_schemas,
                                                       schema_index + 1);
     bool transform_onleave = false;
     char *transform_leave;
     const xmlNode *xml = original_xml;
     xmlNode *upgrade = NULL;
     xmlNode *final = NULL;
     xmlRelaxNGValidityErrorFunc error_handler = NULL;
 
     CRM_ASSERT((schema != NULL) && (upgraded_schema != NULL));
 
     if (to_logs) {
         error_handler = (xmlRelaxNGValidityErrorFunc) xml_log;
     }
 
     transform_onleave = schema->transform_onleave;
     if (schema->transform_enter != NULL) {
         crm_debug("Upgrading schema from %s to %s: "
                   "applying pre-upgrade XSL transform %s",
                   schema->name, upgraded_schema->name, schema->transform_enter);
         upgrade = apply_transformation(xml, schema->transform_enter, to_logs);
         if (upgrade == NULL) {
             crm_warn("Pre-upgrade XSL transform %s failed, "
                      "will skip post-upgrade transform",
                      schema->transform_enter);
             transform_onleave = FALSE;
         } else {
             xml = upgrade;
         }
     }
 
 
     crm_debug("Upgrading schema from %s to %s: "
               "applying upgrade XSL transform %s",
               schema->name, upgraded_schema->name, schema->transform);
     final = apply_transformation(xml, schema->transform, to_logs);
     if (upgrade != xml) {
         pcmk__xml_free(upgrade);
         upgrade = NULL;
     }
 
     if ((final != NULL) && transform_onleave) {
         upgrade = final;
         /* following condition ensured in add_schema_by_version */
         CRM_ASSERT(schema->transform_enter != NULL);
         transform_leave = strdup(schema->transform_enter);
         /* enter -> leave */
         memcpy(strrchr(transform_leave, '-') + 1, "leave", sizeof("leave") - 1);
         crm_debug("Upgrading schema from %s to %s: "
                   "applying post-upgrade XSL transform %s",
                   schema->name, upgraded_schema->name, transform_leave);
         final = apply_transformation(upgrade, transform_leave, to_logs);
         if (final == NULL) {
             crm_warn("Ignoring failure of post-upgrade XSL transform %s",
                      transform_leave);
             final = upgrade;
         } else {
             pcmk__xml_free(upgrade);
         }
         free(transform_leave);
     }
 
     if (final == NULL) {
         return NULL;
     }
 
     // Ensure result validates with its new schema
     if (!validate_with(final, upgraded_schema, error_handler,
                        GUINT_TO_POINTER(LOG_ERR))) {
         crm_err("Schema upgrade from %s to %s failed: "
                 "XSL transform %s produced an invalid configuration",
                 schema->name, upgraded_schema->name, schema->transform);
         crm_log_xml_debug(final, "bad-transform-result");
         pcmk__xml_free(final);
         return NULL;
     }
 
     crm_info("Schema upgrade from %s to %s succeeded",
              schema->name, upgraded_schema->name);
     return final;
 }
 
 /*!
  * \internal
  * \brief Get the schema list entry corresponding to XML configuration
  *
  * \param[in] xml  CIB XML to check
  *
  * \return List entry of schema configured in \p xml
  */
 static GList *
 get_configured_schema(const xmlNode *xml)
 {
     const char *schema_name = crm_element_value(xml, PCMK_XA_VALIDATE_WITH);
 
     pcmk__warn_if_schema_deprecated(schema_name);
     if (schema_name == NULL) {
         return NULL;
     }
     return pcmk__get_schema(schema_name);
 }
 
 /*!
  * \brief Update CIB XML to latest schema that validates it
  *
  * \param[in,out] xml              XML to update (may be freed and replaced
  *                                 after being transformed)
  * \param[in]     max_schema_name  If not NULL, do not update \p xml to any
  *                                 schema later than this one
  * \param[in]     transform        If false, do not update \p xml to any schema
  *                                 that requires an XSL transform
  * \param[in]     to_logs          If false, certain validation errors will be
  *                                 sent to stderr rather than logged
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__update_schema(xmlNode **xml, const char *max_schema_name, bool transform,
                     bool to_logs)
 {
     int max_stable_schemas = xml_latest_schema_index();
     int max_schema_index = 0;
     int rc = pcmk_rc_ok;
     GList *entry = NULL;
     pcmk__schema_t *best_schema = NULL;
     pcmk__schema_t *original_schema = NULL;
     xmlRelaxNGValidityErrorFunc error_handler = 
         to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL;
 
     CRM_CHECK((xml != NULL) && (*xml != NULL) && ((*xml)->doc != NULL),
               return EINVAL);
 
     if (max_schema_name != NULL) {
         GList *max_entry = pcmk__get_schema(max_schema_name);
 
         if (max_entry != NULL) {
             pcmk__schema_t *max_schema = max_entry->data;
 
             max_schema_index = max_schema->schema_index;
         }
     }
     if ((max_schema_index < 1) || (max_schema_index > max_stable_schemas)) {
         max_schema_index = max_stable_schemas;
     }
 
     entry = get_configured_schema(*xml);
     if (entry == NULL) {
         // @COMPAT Not specifying a schema name is deprecated since 2.1.8
         entry = known_schemas;
     } else {
         original_schema = entry->data;
         if (original_schema->schema_index >= max_schema_index) {
             return pcmk_rc_ok;
         }
     }
 
     for (; entry != NULL; entry = entry->next) {
         pcmk__schema_t *current_schema = entry->data;
         xmlNode *upgrade = NULL;
 
         if (current_schema->schema_index > max_schema_index) {
             break;
         }
 
         if (!validate_with(*xml, current_schema, error_handler,
                            GUINT_TO_POINTER(LOG_ERR))) {
             crm_debug("Schema %s does not validate", current_schema->name);
             if (best_schema != NULL) {
                 /* we've satisfied the validation, no need to check further */
                 break;
             }
             rc = pcmk_rc_schema_validation;
             continue; // Try again with the next higher schema
         }
 
         crm_debug("Schema %s validates", current_schema->name);
         rc = pcmk_rc_ok;
         best_schema = current_schema;
         if (current_schema->schema_index == max_schema_index) {
             break; // No further transformations possible
         }
 
         if (!transform || (current_schema->transform == NULL)
             || validate_with_silent(*xml, entry->next->data)) {
             /* The next schema either doesn't require a transform or validates
              * successfully even without the transform. Skip the transform and
              * try the next schema with the same XML.
              */
             continue;
         }
 
         upgrade = apply_upgrade(*xml, current_schema->schema_index, to_logs);
         if (upgrade == NULL) {
             /* The transform failed, so this schema can't be used. Later
              * schemas are unlikely to validate, but try anyway until we
              * run out of options.
              */
             rc = pcmk_rc_transform_failed;
         } else {
             best_schema = current_schema;
             pcmk__xml_free(*xml);
             *xml = upgrade;
         }
     }
 
     if (best_schema != NULL) {
         if ((original_schema == NULL)
             || (best_schema->schema_index > original_schema->schema_index)) {
             crm_info("%s the configuration schema to %s",
                      (transform? "Transformed" : "Upgraded"),
                      best_schema->name);
             crm_xml_add(*xml, PCMK_XA_VALIDATE_WITH, best_schema->name);
         }
     }
     return rc;
 }
 
 /*!
  * \brief Update XML from its configured schema to the latest major series
  *
  * \param[in,out] xml      XML to update
  * \param[in]     to_logs  If false, certain validation errors will be
  *                         sent to stderr rather than logged
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk_update_configured_schema(xmlNode **xml, bool to_logs)
 {
     int rc = pcmk_rc_ok;
     char *original_schema_name = NULL;
 
     // @COMPAT Not specifying a schema name is deprecated since 2.1.8
     const char *effective_original_name = "the first";
 
     int orig_version = -1;
     pcmk__schema_t *x_0_schema = pcmk__find_x_0_schema()->data;
     GList *entry = NULL;
 
     CRM_CHECK(xml != NULL, return EINVAL);
 
     original_schema_name = crm_element_value_copy(*xml, PCMK_XA_VALIDATE_WITH);
     pcmk__warn_if_schema_deprecated(original_schema_name);
     entry = pcmk__get_schema(original_schema_name);
     if (entry != NULL) {
         pcmk__schema_t *original_schema = entry->data;
 
         effective_original_name = original_schema->name;
         orig_version = original_schema->schema_index;
     }
 
     if (orig_version < x_0_schema->schema_index) {
         // Current configuration schema is not acceptable, try to update
         xmlNode *converted = NULL;
         const char *new_schema_name = NULL;
         pcmk__schema_t *schema = NULL;
 
         entry = NULL;
         converted = pcmk__xml_copy(NULL, *xml);
         if (pcmk__update_schema(&converted, NULL, true, to_logs) == pcmk_rc_ok) {
             new_schema_name = crm_element_value(converted,
                                                 PCMK_XA_VALIDATE_WITH);
             entry = pcmk__get_schema(new_schema_name);
         }
         schema = (entry == NULL)? NULL : entry->data;
 
         if ((schema == NULL)
             || (schema->schema_index < x_0_schema->schema_index)) {
             // Updated configuration schema is still not acceptable
 
             if ((orig_version == -1) || (schema == NULL)
                 || (schema->schema_index < orig_version)) {
                 // We couldn't validate any schema at all
                 if (to_logs) {
                     pcmk__config_err("Cannot upgrade configuration (claiming "
                                      "%s schema) to at least %s because it "
                                      "does not validate with any schema from "
                                      "%s to the latest",
                                      pcmk__s(original_schema_name, "no"),
                                      x_0_schema->name, effective_original_name);
                 } else {
                     fprintf(stderr, "Cannot upgrade configuration (claiming "
                                     "%s schema) to at least %s because it "
                                     "does not validate with any schema from "
                                     "%s to the latest\n",
                                     pcmk__s(original_schema_name, "no"),
                                     x_0_schema->name, effective_original_name);
                 }
             } else {
                 // We updated configuration successfully, but still too low
                 if (to_logs) {
                     pcmk__config_err("Cannot upgrade configuration (claiming "
                                      "%s schema) to at least %s because it "
                                      "would not upgrade past %s",
                                      pcmk__s(original_schema_name, "no"),
                                      x_0_schema->name,
                                      pcmk__s(new_schema_name, "unspecified version"));
                 } else {
                     fprintf(stderr, "Cannot upgrade configuration (claiming "
                                     "%s schema) to at least %s because it "
                                     "would not upgrade past %s\n",
                                     pcmk__s(original_schema_name, "no"),
                                     x_0_schema->name,
                                     pcmk__s(new_schema_name, "unspecified version"));
                 }
             }
 
             pcmk__xml_free(converted);
             converted = NULL;
             rc = pcmk_rc_transform_failed;
 
         } else {
             // Updated configuration schema is acceptable
             pcmk__xml_free(*xml);
             *xml = converted;
 
             if (schema->schema_index < xml_latest_schema_index()) {
                 if (to_logs) {
                     pcmk__config_warn("Configuration with %s schema was "
                                       "internally upgraded to acceptable (but "
                                       "not most recent) %s",
                                       pcmk__s(original_schema_name, "no"),
                                       schema->name);
                 }
             } else if (to_logs) {
                 crm_info("Configuration with %s schema was internally "
                          "upgraded to latest version %s",
                          pcmk__s(original_schema_name, "no"),
                          schema->name);
             }
         }
 
     } else {
         // @COMPAT the none schema is deprecated since 2.1.8
         pcmk__schema_t *none_schema = NULL;
 
         entry = pcmk__get_schema(PCMK_VALUE_NONE);
         CRM_ASSERT((entry != NULL) && (entry->data != NULL));
 
         none_schema = entry->data;
         if (!to_logs && (orig_version >= none_schema->schema_index)) {
             fprintf(stderr, "Schema validation of configuration is "
                             "disabled (support for " PCMK_XA_VALIDATE_WITH
                             " set to \"" PCMK_VALUE_NONE "\" is deprecated"
                             " and will be removed in a future release)\n");
         }
     }
 
     free(original_schema_name);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Return a list of all schema files and any associated XSLT files
  *        later than the given one
  * \brief Return a list of all schema versions later than the given one
  *
  * \param[in] schema The schema to compare against (for example,
  *                   "pacemaker-3.1.rng" or "pacemaker-3.1")
  *
  * \note The caller is responsible for freeing both the returned list and
  *       the elements of the list
  */
 GList *
 pcmk__schema_files_later_than(const char *name)
 {
     GList *lst = NULL;
     pcmk__schema_version_t ver;
 
     if (!version_from_filename(name, &ver)) {
         return lst;
     }
 
     for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index());
          iter != NULL; iter = iter->prev) {
         pcmk__schema_t *schema = iter->data;
         char *s = NULL;
 
         if (schema_cmp(ver, schema->version) != -1) {
             continue;
         }
 
         s = crm_strdup_printf("%s.rng", schema->name);
         lst = g_list_prepend(lst, s);
 
         if (schema->transform != NULL) {
             char *xform = crm_strdup_printf("%s.xsl", schema->transform);
             lst = g_list_prepend(lst, xform);
         }
 
         if (schema->transform_enter != NULL) {
             char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter);
 
             lst = g_list_prepend(lst, enter);
 
             if (schema->transform_onleave) {
                 int last_dash = strrchr(enter, '-') - enter;
                 char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter);
 
                 lst = g_list_prepend(lst, leave);
             }
         }
     }
 
     return lst;
 }
 
 static void
 append_href(xmlNode *xml, void *user_data)
 {
     GList **list = user_data;
     char *href = crm_element_value_copy(xml, "href");
 
     if (href == NULL) {
         return;
     }
     *list = g_list_prepend(*list, href);
 }
 
 static void
 external_refs_in_schema(GList **list, const char *contents)
 {
     /* local-name()= is needed to ignore the xmlns= setting at the top of
      * the XML file.  Otherwise, the xpath query will always return nothing.
      */
     const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']";
     xmlNode *xml = pcmk__xml_parse(contents);
 
     crm_foreach_xpath_result(xml, search, append_href, list);
     pcmk__xml_free(xml);
 }
 
 static int
 read_file_contents(const char *file, char **contents)
 {
     int rc = pcmk_rc_ok;
     char *path = NULL;
 
     if (pcmk__ends_with(file, ".rng")) {
         path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file);
     } else {
         path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file);
     }
 
     rc = pcmk__file_contents(path, contents);
 
     free(path);
     return rc;
 }
 
 static void
 add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included)
 {
     char *contents = NULL;
     char *path = NULL;
     xmlNode *file_node = NULL;
     GList *includes = NULL;
     int rc = pcmk_rc_ok;
 
     /* If we already included this file, don't do so again. */
     if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) {
         return;
     }
 
     /* Ensure whatever file we were given has a suffix we know about.  If not,
      * just assume it's an RNG file.
      */
     if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) {
         path = crm_strdup_printf("%s.rng", file);
     } else {
         path = pcmk__str_copy(file);
     }
 
     rc = read_file_contents(path, &contents);
     if (rc != pcmk_rc_ok || contents == NULL) {
         crm_warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc));
         free(path);
         return;
     }
 
     /* Create a new <file path="..."> node with the contents of the file
      * as a CDATA block underneath it.
      */
     file_node = pcmk__xe_create(parent, PCMK_XA_FILE);
     crm_xml_add(file_node, PCMK_XA_PATH, path);
     *already_included = g_list_prepend(*already_included, path);
 
     xmlAddChild(file_node, xmlNewCDataBlock(parent->doc, (pcmkXmlStr) contents,
                                             strlen(contents)));
 
     /* Scan the file for any <externalRef> or <include> nodes and build up
      * a list of the files they reference.
      */
     external_refs_in_schema(&includes, contents);
 
     /* For each referenced file, recurse to add it (and potentially anything it
      * references, ...) to the XML.
      */
     for (GList *iter = includes; iter != NULL; iter = iter->next) {
         add_schema_file_to_xml(parent, iter->data, already_included);
     }
 
     free(contents);
     g_list_free_full(includes, free);
 }
 
 /*!
  * \internal
  * \brief Add an XML schema file and all the files it references as children
  *        of a given XML node
  *
  * \param[in,out] parent            The parent XML node
  * \param[in] name                  The schema version to compare against
  *                                  (for example, "pacemaker-3.1" or "pacemaker-3.1.rng")
  * \param[in,out] already_included  A list of names that have already been added
  *                                  to the parent node.
  *
  * \note The caller is responsible for freeing both the returned list and
  *       the elements of the list
  */
 void
 pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included)
 {
     xmlNode *schema_node = pcmk__xe_create(parent, PCMK__XA_SCHEMA);
 
     crm_xml_add(schema_node, PCMK_XA_VERSION, name);
     add_schema_file_to_xml(schema_node, name, already_included);
 
     if (schema_node->children == NULL) {
         // Not needed if empty. May happen if name was invalid, for example.
         pcmk__xml_free(schema_node);
     }
 }
 
 /*!
  * \internal
  * \brief Return the directory containing any extra schema files that a
  *        Pacemaker Remote node fetched from the cluster
  */
 const char *
 pcmk__remote_schema_dir(void)
 {
     const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY);
 
     if (pcmk__str_empty(dir)) {
         return PCMK__REMOTE_SCHEMA_DIR;
     }
 
     return dir;
 }
 
 /*!
  * \internal
  * \brief Warn if a given validation schema is deprecated
  *
  * \param[in] Schema name to check
  */
 void
 pcmk__warn_if_schema_deprecated(const char *schema)
 {
     if ((schema == NULL) ||
         pcmk__strcase_any_of(schema, "pacemaker-next", PCMK_VALUE_NONE, NULL)) {
         pcmk__config_warn("Support for " PCMK_XA_VALIDATE_WITH "='%s' is "
                           "deprecated and will be removed in a future release "
                           "without the possibility of upgrades (manually edit "
                           "to use a supported schema)", pcmk__s(schema, ""));
     }
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/common/xml_compat.h>
 
-const char *
-xml_latest_schema(void)
-{
-    return pcmk__highest_schema_name();
-}
-
-const char *
-get_schema_name(int version)
-{
-    pcmk__schema_t *schema = g_list_nth_data(known_schemas, version);
-
-    return (schema != NULL)? schema->name : "unknown";
-}
-
-int
-get_schema_version(const char *name)
-{
-    int lpc = 0;
-
-    if (name == NULL) {
-        name = PCMK_VALUE_NONE;
-    }
-
-    for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
-        pcmk__schema_t *schema = iter->data;
-
-        if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
-            return lpc;
-        }
-
-        lpc++;
-    }
-
-    return -1;
-}
-
-int
-update_validation(xmlNode **xml, int *best, int max, gboolean transform,
-                  gboolean to_logs)
-{
-    int rc = pcmk__update_schema(xml, get_schema_name(max), transform, to_logs);
-
-    if ((best != NULL) && (xml != NULL) && (rc == pcmk_rc_ok)) {
-        const char *schema_name = crm_element_value(*xml,
-                                                    PCMK_XA_VALIDATE_WITH);
-        GList *schema_entry = pcmk__get_schema(schema_name);
-
-        if (schema_entry != NULL) {
-            *best = ((pcmk__schema_t *)(schema_entry->data))->schema_index;
-        }
-    }
-
-    return pcmk_rc2legacy(rc);
-}
-
-gboolean
-validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs)
-{
-    bool rc = pcmk__validate_xml(xml_blob, validation,
-                                 to_logs? (xmlRelaxNGValidityErrorFunc) xml_log : NULL,
-                                 GUINT_TO_POINTER(LOG_ERR));
-    return rc? TRUE : FALSE;
-}
-
-static void
-dump_file(const char *filename)
-{
-
-    FILE *fp = NULL;
-    int ch, line = 0;
-
-    CRM_CHECK(filename != NULL, return);
-
-    fp = fopen(filename, "r");
-    if (fp == NULL) {
-        crm_perror(LOG_ERR, "Could not open %s for reading", filename);
-        return;
-    }
-
-    fprintf(stderr, "%4d ", ++line);
-    do {
-        ch = getc(fp);
-        if (ch == EOF) {
-            putc('\n', stderr);
-            break;
-        } else if (ch == '\n') {
-            fprintf(stderr, "\n%4d ", ++line);
-        } else {
-            putc(ch, stderr);
-        }
-    } while (1);
-
-    fclose(fp);
-}
-
-gboolean
-validate_xml_verbose(const xmlNode *xml_blob)
-{
-    int fd = 0;
-    xmlDoc *doc = NULL;
-    xmlNode *xml = NULL;
-    gboolean rc = FALSE;
-    char *filename = NULL;
-
-    filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir());
-
-    umask(S_IWGRP | S_IWOTH | S_IROTH);
-    fd = mkstemp(filename);
-    pcmk__xml_write_fd(xml_blob, filename, fd, false, NULL);
-
-    dump_file(filename);
-
-    doc = xmlReadFile(filename, NULL, 0);
-    xml = xmlDocGetRootElement(doc);
-    rc = pcmk__validate_xml(xml, NULL, NULL, NULL);
-    pcmk__xml_free(xml);
-
-    unlink(filename);
-    free(filename);
-
-    return rc? TRUE : FALSE;
-}
-
 gboolean
 cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs)
 {
     int rc = pcmk_update_configured_schema(xml, to_logs);
 
     if (best_version != NULL) {
         const char *name = crm_element_value(*xml, PCMK_XA_VALIDATE_WITH);
 
         if (name == NULL) {
             *best_version = -1;
         } else {
             GList *entry = pcmk__get_schema(name);
             pcmk__schema_t *schema = (entry == NULL)? NULL : entry->data;
 
             *best_version = (schema == NULL)? -1 : schema->schema_index;
         }
     }
     return (rc == pcmk_rc_ok)? TRUE: FALSE;
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/lib/common/xml.c b/lib/common/xml.c
index ea9621f79d..b0ca4d4230 100644
--- a/lib/common/xml.c
+++ b/lib/common/xml.c
@@ -1,2793 +1,2400 @@
 /*
  * 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 <crm_internal.h>
 
 #include <stdarg.h>
 #include <stdint.h>                     // uint32_t
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/stat.h>                   // stat(), S_ISREG, etc.
 #include <sys/types.h>
 
 #include <libxml/parser.h>
 #include <libxml/tree.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>    // PCMK__XML_LOG_BASE, etc.
 #include "crmcommon_private.h"
 
 /*!
  * \internal
  * \brief Apply a function to each XML node in a tree (pre-order, depth-first)
  *
  * \param[in,out] xml        XML tree to traverse
  * \param[in,out] fn         Function to call for each node (returns \c true to
  *                           continue traversing the tree or \c false to stop)
  * \param[in,out] user_data  Argument to \p fn
  *
  * \return \c false if any \p fn call returned \c false, or \c true otherwise
  *
  * \note This function is recursive.
  */
 bool
 pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
                        void *user_data)
 {
     if (!fn(xml, user_data)) {
         return false;
     }
 
     for (xml = pcmk__xml_first_child(xml); xml != NULL;
          xml = pcmk__xml_next(xml)) {
 
         if (!pcmk__xml_tree_foreach(xml, fn, user_data)) {
             return false;
         }
     }
     return true;
 }
 
 bool
 pcmk__tracking_xml_changes(xmlNode *xml, bool lazy)
 {
     if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) {
         return FALSE;
     } else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags,
                             pcmk__xf_tracking)) {
         return FALSE;
     } else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags,
                                     pcmk__xf_lazy)) {
         return FALSE;
     }
     return TRUE;
 }
 
 static inline void
 set_parent_flag(xmlNode *xml, long flag) 
 {
     for(; xml; xml = xml->parent) {
         xml_node_private_t *nodepriv = xml->_private;
 
         if (nodepriv == NULL) {
             /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */
         } else {
             pcmk__set_xml_flags(nodepriv, flag);
         }
     }
 }
 
 void
 pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag)
 {
     if(xml && xml->doc && xml->doc->_private){
         /* During calls to xmlDocCopyNode(), xml->doc may be unset */
         xml_doc_private_t *docpriv = xml->doc->_private;
 
         pcmk__set_xml_flags(docpriv, flag);
     }
 }
 
 // Mark document, element, and all element's parents as changed
 void
 pcmk__mark_xml_node_dirty(xmlNode *xml)
 {
     pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty);
     set_parent_flag(xml, pcmk__xf_dirty);
 }
 
 /*!
  * \internal
  * \brief Clear flags on an XML node
  *
  * \param[in,out] xml        XML node whose flags to reset
  * \param[in,out] user_data  Ignored
  *
  * \return \c true (to continue traversing the tree)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 reset_xml_node_flags(xmlNode *xml, void *user_data)
 {
     xml_node_private_t *nodepriv = xml->_private;
 
     if (nodepriv != NULL) {
         nodepriv->flags = pcmk__xf_none;
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node
  *
  * \param[in,out] xml        Node whose flags to set
  * \param[in]     user_data  Ignored
  *
  * \return \c true (to continue traversing the tree)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 mark_xml_dirty_created(xmlNode *xml, void *user_data)
 {
     xml_node_private_t *nodepriv = xml->_private;
 
     if (nodepriv != NULL) {
         pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Mark an XML tree as dirty and created, and mark its parents dirty
  *
  * Also mark the document dirty.
  *
  * \param[in,out] xml  Tree to mark as dirty and created
  */
 void
 pcmk__xml_mark_created(xmlNode *xml)
 {
     CRM_ASSERT(xml != NULL);
 
     if (!pcmk__tracking_xml_changes(xml, false)) {
         // Tracking is disabled for entire document
         return;
     }
 
     // Mark all parents and document dirty
     pcmk__mark_xml_node_dirty(xml);
 
     pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL);
 }
 
 #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;
 
         g_free(deleted_obj->path);
         free(deleted_obj);
     }
 }
 
 // Free and NULL user, ACLs, and deleted objects in an XML node's private data
 static void
 reset_xml_private_data(xml_doc_private_t *docpriv)
 {
     if (docpriv != NULL) {
         CRM_ASSERT(docpriv->check == 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 =
                 pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
 
             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 =
                 pcmk__assert_alloc(1, sizeof(xml_node_private_t));
 
             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;
 }
 
 /*!
  * \internal
  * \brief Remove all attributes marked as deleted from an XML node
  *
  * \param[in,out] xml        XML node whose deleted attributes to remove
  * \param[in,out] user_data  Ignored
  *
  * \return \c true (to continue traversing the tree)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 accept_attr_deletions(xmlNode *xml, void *user_data)
 {
     reset_xml_node_flags(xml, NULL);
     pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL);
     return true;
 }
 
 /*!
  * \internal
  * \brief Find first child XML node matching another given XML node
  *
  * \param[in] haystack  XML whose children should be checked
  * \param[in] needle    XML to match (comment content or element name and ID)
  * \param[in] exact     If true and needle is a comment, position must match
  */
 xmlNode *
 pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact)
 {
     CRM_CHECK(needle != NULL, return NULL);
 
     if (needle->type == XML_COMMENT_NODE) {
         return pcmk__xc_match(haystack, needle, exact);
 
     } else {
         const char *id = pcmk__xe_id(needle);
         const char *attr = (id == NULL)? NULL : PCMK_XA_ID;
 
         return pcmk__xe_first_child(haystack, (const char *) needle->name, attr,
                                     id);
     }
 }
 
 void
 xml_accept_changes(xmlNode * xml)
 {
     xmlNode *top = NULL;
     xml_doc_private_t *docpriv = NULL;
 
     if(xml == NULL) {
         return;
     }
 
     crm_trace("Accepting changes to %p", xml);
     docpriv = xml->doc->_private;
     top = xmlDocGetRootElement(xml->doc);
 
     reset_xml_private_data(xml->doc->_private);
 
     if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) {
         docpriv->flags = pcmk__xf_none;
         return;
     }
 
     docpriv->flags = pcmk__xf_none;
     pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL);
 }
 
 /*!
  * \internal
  * \brief Find first XML child element matching given criteria
  *
  * \param[in] parent     XML element to search (can be \c NULL)
  * \param[in] node_name  If not \c NULL, only match children of this type
  * \param[in] attr_n     If not \c NULL, only match children with an attribute
  *                       of this name.
  * \param[in] attr_v     If \p attr_n and this are not NULL, only match children
  *                       with an attribute named \p attr_n and this value
  *
  * \return Matching XML child element, or \c NULL if none found
  */
 xmlNode *
 pcmk__xe_first_child(const xmlNode *parent, const char *node_name,
                      const char *attr_n, const char *attr_v)
 {
     xmlNode *child = NULL;
     const char *parent_name = "<null>";
 
     CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL);
 
     if (parent != NULL) {
         child = parent->children;
         while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) {
             child = child->next;
         }
 
         parent_name = (const char *) parent->name;
     }
 
     for (; child != NULL; child = pcmk__xe_next(child)) {
         const char *value = NULL;
 
         if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) {
             // Node name mismatch
             continue;
         }
         if (attr_n == NULL) {
             // No attribute match needed
             return child;
         }
 
         value = crm_element_value(child, attr_n);
 
         if ((attr_v == NULL) && (value != NULL)) {
             // attr_v == NULL: Attribute attr_n must be set (to any value)
             return child;
         }
         if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) {
             // attr_v != NULL: Attribute attr_n must be set to value attr_v
             return child;
         }
     }
 
     if (node_name == NULL) {
         node_name = "(any)";    // For logging
     }
     if (attr_n != NULL) {
         crm_trace("XML child node <%s %s=%s> not found in %s",
                   node_name, attr_n, attr_v, parent_name);
     } else {
         crm_trace("XML child node <%s> not found in %s",
                   node_name, parent_name);
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Set an XML attribute, expanding \c ++ and \c += where appropriate
  *
  * If \p target already has an attribute named \p name set to an integer value
  * and \p value is an addition assignment expression on \p name, then expand
  * \p value to an integer and set attribute \p name to the expanded value in
  * \p target.
  *
  * Otherwise, set attribute \p name on \p target using the literal \p value.
  *
  * The original attribute value in \p target and the number in an assignment
  * expression in \p value are parsed and added as scores (that is, their values
  * are capped at \c INFINITY and \c -INFINITY). For more details, refer to
  * \c char2score().
  *
  * For example, suppose \p target has an attribute named \c "X" with value
  * \c "5", and that \p name is \c "X".
  * * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6".
  * * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8".
  * * If \p value is \c "val", the new value of \c "X" in \p target is \c "val".
  * * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++".
  *
  * \param[in,out] target  XML node whose attribute to set
  * \param[in]     name    Name of the attribute to set
  * \param[in]     value   New value of attribute to set
  *
  * \return Standard Pacemaker return code (specifically, \c EINVAL on invalid
  *         argument, or \c pcmk_rc_ok otherwise)
  */
 int
 pcmk__xe_set_score(xmlNode *target, const char *name, const char *value)
 {
     const char *old_value = NULL;
 
     CRM_CHECK((target != NULL) && (name != NULL), return EINVAL);
 
     if (value == NULL) {
         return pcmk_rc_ok;
     }
 
     old_value = crm_element_value(target, name);
 
     // If no previous value, skip to default case and set the value unexpanded.
     if (old_value != NULL) {
         const char *n = name;
         const char *v = value;
 
         // Stop at first character that differs between name and value
         for (; (*n == *v) && (*n != '\0'); n++, v++);
 
         // If value begins with name followed by a "++" or "+="
         if ((*n == '\0')
             && (*v++ == '+')
             && ((*v == '+') || (*v == '='))) {
 
             // If we're expanding ourselves, no previous value was set; use 0
             int old_value_i = (old_value != value)? char2score(old_value) : 0;
 
             /* value="X++": new value of X is old_value + 1
              * value="X+=Y": new value of X is old_value + Y (for some number Y)
              */
             int add = (*v == '+')? 1 : char2score(++v);
 
             crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add));
             return pcmk_rc_ok;
         }
     }
 
     // Default case: set the attribute unexpanded (with value treated literally)
     if (old_value != value) {
         crm_xml_add(target, name, value);
     }
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Copy XML attributes from a source element to a target element
  *
  * This is similar to \c xmlCopyPropList() except that attributes are marked
  * as dirty for change tracking purposes.
  *
  * \param[in,out] target  XML element to receive copied attributes from \p src
  * \param[in]     src     XML element whose attributes to copy to \p target
  * \param[in]     flags   Group of <tt>enum pcmk__xa_flags</tt>
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags)
 {
     CRM_CHECK((src != NULL) && (target != NULL), return EINVAL);
 
     for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL;
          attr = attr->next) {
 
         const char *name = (const char *) attr->name;
         const char *value = pcmk__xml_attr_value(attr);
 
         if (pcmk_is_set(flags, pcmk__xaf_no_overwrite)
             && (crm_element_value(target, name) != NULL)) {
             continue;
         }
 
         if (pcmk_is_set(flags, pcmk__xaf_score_update)) {
             pcmk__xe_set_score(target, name, value);
         } else {
             crm_xml_add(target, name, value);
         }
     }
 
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Remove an XML attribute from an element
  *
  * \param[in,out] element  XML element that owns \p attr
  * \param[in,out] attr     XML attribute to remove from \p element
  *
  * \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal of
  *         attributes from \p element, or \c pcmk_rc_ok otherwise)
  */
 static int
 remove_xe_attr(xmlNode *element, xmlAttr *attr)
 {
     if (attr == NULL) {
         return pcmk_rc_ok;
     }
 
     if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) {
         // ACLs apply to element, not to particular attributes
         crm_trace("ACLs prevent removal of attributes from %s element",
                   (const char *) element->name);
         return EPERM;
     }
 
     if (pcmk__tracking_xml_changes(element, false)) {
         // Leave in place (marked for removal) until after diff is calculated
         set_parent_flag(element, pcmk__xf_dirty);
         pcmk__set_xml_flags((xml_node_private_t *) attr->_private,
                             pcmk__xf_deleted);
     } else {
         xmlRemoveProp(attr);
     }
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Remove a named attribute from an XML element
  *
  * \param[in,out] element  XML element to remove an attribute from
  * \param[in]     name     Name of attribute to remove
  */
 void
 pcmk__xe_remove_attr(xmlNode *element, const char *name)
 {
     if (name != NULL) {
         remove_xe_attr(element, xmlHasProp(element, (pcmkXmlStr) name));
     }
 }
 
 /*!
  * \internal
  * \brief Remove a named attribute from an XML element
  *
  * This is a wrapper for \c pcmk__xe_remove_attr() for use with
  * \c pcmk__xml_tree_foreach().
  *
  * \param[in,out] xml        XML element to remove an attribute from
  * \param[in]     user_data  Name of attribute to remove
  *
  * \return \c true (to continue traversing the tree)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 bool
 pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data)
 {
     const char *name = user_data;
 
     pcmk__xe_remove_attr(xml, name);
     return true;
 }
 
 /*!
  * \internal
  * \brief Remove an XML element's attributes that match some criteria
  *
  * \param[in,out] element    XML element to modify
  * \param[in]     match      If not NULL, only remove attributes for which
  *                           this function returns true
  * \param[in,out] user_data  Data to pass to \p match
  */
 void
 pcmk__xe_remove_matching_attrs(xmlNode *element,
                                bool (*match)(xmlAttrPtr, void *),
                                void *user_data)
 {
     xmlAttrPtr next = NULL;
 
     for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) {
         next = a->next; // Grab now because attribute might get removed
         if ((match == NULL) || match(a, user_data)) {
             if (remove_xe_attr(element, a) != pcmk_rc_ok) {
                 return;
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief Create a new XML element under a given parent
  *
  * \param[in,out] parent  XML element that will be the new element's parent
  *                        (\c NULL to create a new XML document with the new
  *                        node as root)
  * \param[in]     name    Name of new element
  *
  * \return Newly created XML element (guaranteed not to be \c NULL)
  */
 xmlNode *
 pcmk__xe_create(xmlNode *parent, const char *name)
 {
     xmlNode *node = NULL;
 
     CRM_ASSERT(!pcmk__str_empty(name));
 
     if (parent == NULL) {
         xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION);
 
         pcmk__mem_assert(doc);
 
         node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL);
         pcmk__mem_assert(node);
 
         xmlDocSetRootElement(doc, node);
 
     } else {
         node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL);
         pcmk__mem_assert(node);
     }
 
     pcmk__xml_mark_created(node);
     return node;
 }
 
 /*!
  * \internal
  * \brief Set a formatted string as an XML node's content
  *
  * \param[in,out] node    Node whose content to set
  * \param[in]     format  <tt>printf(3)</tt>-style format string
  * \param[in]     ...     Arguments for \p format
  *
  * \note This function escapes special characters. \c xmlNodeSetContent() does
  *       not.
  */
 G_GNUC_PRINTF(2, 3)
 void
 pcmk__xe_set_content(xmlNode *node, const char *format, ...)
 {
     if (node != NULL) {
         const char *content = NULL;
         char *buf = NULL;
 
         if (strchr(format, '%') == NULL) {
             // Nothing to format
             content = format;
 
         } else {
             va_list ap;
 
             va_start(ap, format);
 
             if (pcmk__str_eq(format, "%s", pcmk__str_none)) {
                 // No need to make a copy
                 content = va_arg(ap, const char *);
 
             } else {
                 CRM_ASSERT(vasprintf(&buf, format, ap) >= 0);
                 content = buf;
             }
             va_end(ap);
         }
 
         xmlNodeSetContent(node, (pcmkXmlStr) content);
         free(buf);
     }
 }
 
 /*!
  * \internal
  * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled
  *
  * If \p node is the root of its document, free the entire document.
  *
  * \param[in,out] node      XML node to free
  * \param[in]     position  Position of \p node among its siblings for change
  *                          tracking (negative to calculate automatically if
  *                          needed)
  */
 static void
 free_xml_with_position(xmlNode *node, int position)
 {
     xmlDoc *doc = NULL;
     xml_node_private_t *nodepriv = NULL;
 
     if (node == NULL) {
         return;
     }
     doc = node->doc;
     nodepriv = node->_private;
 
     if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) {
         /* @TODO Should we check ACLs first? Otherwise it seems like we could
          * free the root element without write permission.
          */
         xmlFreeDoc(doc);
         return;
     }
 
     if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) {
         GString *xpath = NULL;
 
         pcmk__if_tracing({}, return);
         xpath = pcmk__element_xpath(node);
         qb_log_from_external_source(__func__, __FILE__,
                                     "Cannot remove %s %x", LOG_TRACE,
                                     __LINE__, 0, xpath->str, nodepriv->flags);
         g_string_free(xpath, TRUE);
         return;
     }
 
     if ((doc != NULL) && pcmk__tracking_xml_changes(node, false)
         && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
 
         xml_doc_private_t *docpriv = doc->_private;
         GString *xpath = pcmk__element_xpath(node);
 
         if (xpath != NULL) {
             pcmk__deleted_xml_t *deleted_obj = NULL;
 
             crm_trace("Deleting %s %p from %p", xpath->str, node, doc);
 
             deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t));
             deleted_obj->path = g_string_free(xpath, FALSE);
             deleted_obj->position = -1;
 
             // Record the position only for XML comments for now
             if (node->type == XML_COMMENT_NODE) {
                 if (position >= 0) {
                     deleted_obj->position = position;
 
                 } else {
                     deleted_obj->position = pcmk__xml_position(node,
                                                                pcmk__xf_skip);
                 }
             }
 
             docpriv->deleted_objs = g_list_append(docpriv->deleted_objs,
                                                   deleted_obj);
             pcmk__set_xml_doc_flag(node, pcmk__xf_dirty);
         }
     }
     xmlUnlinkNode(node);
     xmlFreeNode(node);
 }
 
 /*!
  * \internal
  * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled
  *
  * If \p xml is the root of its document, free the entire document.
  *
  * \param[in,out] xml  XML node to free
  */
 void
 pcmk__xml_free(xmlNode *xml)
 {
     free_xml_with_position(xml, -1);
 }
 
 /*!
  * \internal
  * \brief Make a deep copy of an XML node under a given parent
  *
  * \param[in,out] parent  XML element that will be the copy's parent (\c NULL
  *                        to create a new XML document with the copy as root)
  * \param[in]     src     XML node to copy
  *
  * \return Deep copy of \p src, or \c NULL if \p src is \c NULL
  */
 xmlNode *
 pcmk__xml_copy(xmlNode *parent, xmlNode *src)
 {
     xmlNode *copy = NULL;
 
     if (src == NULL) {
         return NULL;
     }
 
     if (parent == NULL) {
         xmlDoc *doc = NULL;
 
         // The copy will be the root element of a new document
         CRM_ASSERT(src->type == XML_ELEMENT_NODE);
 
         doc = xmlNewDoc(PCMK__XML_VERSION);
         pcmk__mem_assert(doc);
 
         copy = xmlDocCopyNode(src, doc, 1);
         pcmk__mem_assert(copy);
 
         xmlDocSetRootElement(doc, copy);
 
     } else {
         copy = xmlDocCopyNode(src, parent->doc, 1);
         pcmk__mem_assert(copy);
 
         xmlAddChild(parent, copy);
     }
 
     pcmk__xml_mark_created(copy);
     return copy;
 }
 
 /*!
  * \internal
  * \brief Remove XML text nodes from specified XML and all its children
  *
  * \param[in,out] xml  XML to strip text from
  */
 void
 pcmk__strip_xml_text(xmlNode *xml)
 {
     xmlNode *iter = xml->children;
 
     while (iter) {
         xmlNode *next = iter->next;
 
         switch (iter->type) {
             case XML_TEXT_NODE:
                 xmlUnlinkNode(iter);
                 xmlFreeNode(iter);
                 break;
 
             case XML_ELEMENT_NODE:
                 /* Search it */
                 pcmk__strip_xml_text(iter);
                 break;
 
             default:
                 /* Leave it */
                 break;
         }
 
         iter = next;
     }
 }
 
 /*!
  * \internal
  * \brief Add a "last written" attribute to an XML element, set to current time
  *
  * \param[in,out] xe  XML element to add attribute to
  *
  * \return Value that was set, or NULL on error
  */
 const char *
 pcmk__xe_add_last_written(xmlNode *xe)
 {
     char *now_s = pcmk__epoch2str(NULL, 0);
     const char *result = NULL;
 
     result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN,
                          pcmk__s(now_s, "Could not determine current time"));
     free(now_s);
     return result;
 }
 
 /*!
  * \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 Check whether a string has XML special characters that must be escaped
  *
  * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details.
  *
  * \param[in] text  String to check
  * \param[in] type  Type of escaping
  *
  * \return \c true if \p text has special characters that need to be escaped, or
  *         \c false otherwise
  */
 bool
 pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type)
 {
     if (text == NULL) {
         return false;
     }
 
     while (*text != '\0') {
         switch (type) {
             case pcmk__xml_escape_text:
                 switch (*text) {
                     case '<':
                     case '>':
                     case '&':
                         return true;
                     case '\n':
                     case '\t':
                         break;
                     default:
                         if (g_ascii_iscntrl(*text)) {
                             return true;
                         }
                         break;
                 }
                 break;
 
             case pcmk__xml_escape_attr:
                 switch (*text) {
                     case '<':
                     case '>':
                     case '&':
                     case '"':
                         return true;
                     default:
                         if (g_ascii_iscntrl(*text)) {
                             return true;
                         }
                         break;
                 }
                 break;
 
             case pcmk__xml_escape_attr_pretty:
                 switch (*text) {
                     case '\n':
                     case '\r':
                     case '\t':
                     case '"':
                         return true;
                     default:
                         break;
                 }
                 break;
 
             default:    // Invalid enum value
                 CRM_ASSERT(false);
                 break;
         }
 
         text = g_utf8_next_char(text);
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Replace special characters with their XML escape sequences
  *
  * \param[in] text  Text to escape
  * \param[in] type  Type of escaping
  *
  * \return Newly allocated string equivalent to \p text but with special
  *         characters replaced with XML escape sequences (or \c NULL if \p text
  *         is \c NULL). If \p text is not \c NULL, the return value is
  *         guaranteed not to be \c NULL.
  *
  * \note There are libxml functions that purport to do this:
  *       \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars().
  *       However, their escaping is incomplete. See:
  *       https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252
  * \note The caller is responsible for freeing the return value using
  *       \c g_free().
  */
 gchar *
 pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type)
 {
     GString *copy = NULL;
 
     if (text == NULL) {
         return NULL;
     }
     copy = g_string_sized_new(strlen(text));
 
     while (*text != '\0') {
         // Don't escape any non-ASCII characters
         if ((*text & 0x80) != 0) {
             size_t bytes = g_utf8_next_char(text) - text;
 
             g_string_append_len(copy, text, bytes);
             text += bytes;
             continue;
         }
 
         switch (type) {
             case pcmk__xml_escape_text:
                 switch (*text) {
                     case '<':
                         g_string_append(copy, PCMK__XML_ENTITY_LT);
                         break;
                     case '>':
                         g_string_append(copy, PCMK__XML_ENTITY_GT);
                         break;
                     case '&':
                         g_string_append(copy, PCMK__XML_ENTITY_AMP);
                         break;
                     case '\n':
                     case '\t':
                         g_string_append_c(copy, *text);
                         break;
                     default:
                         if (g_ascii_iscntrl(*text)) {
                             g_string_append_printf(copy, "&#x%.2X;", *text);
                         } else {
                             g_string_append_c(copy, *text);
                         }
                         break;
                 }
                 break;
 
             case pcmk__xml_escape_attr:
                 switch (*text) {
                     case '<':
                         g_string_append(copy, PCMK__XML_ENTITY_LT);
                         break;
                     case '>':
                         g_string_append(copy, PCMK__XML_ENTITY_GT);
                         break;
                     case '&':
                         g_string_append(copy, PCMK__XML_ENTITY_AMP);
                         break;
                     case '"':
                         g_string_append(copy, PCMK__XML_ENTITY_QUOT);
                         break;
                     default:
                         if (g_ascii_iscntrl(*text)) {
                             g_string_append_printf(copy, "&#x%.2X;", *text);
                         } else {
                             g_string_append_c(copy, *text);
                         }
                         break;
                 }
                 break;
 
             case pcmk__xml_escape_attr_pretty:
                 switch (*text) {
                     case '"':
                         g_string_append(copy, "\\\"");
                         break;
                     case '\n':
                         g_string_append(copy, "\\n");
                         break;
                     case '\r':
                         g_string_append(copy, "\\r");
                         break;
                     case '\t':
                         g_string_append(copy, "\\t");
                         break;
                     default:
                         g_string_append_c(copy, *text);
                         break;
                 }
                 break;
 
             default:    // Invalid enum value
                 CRM_ASSERT(false);
                 break;
         }
 
         text = g_utf8_next_char(text);
     }
     return g_string_free(copy, FALSE);
 }
 
 /*!
  * \internal
  * \brief Set a flag on all attributes of an XML element
  *
  * \param[in,out] xml   XML node to set flags on
  * \param[in]     flag  XML private flag to set
  */
 static void
 set_attrs_flag(xmlNode *xml, enum xml_private_flags flag)
 {
     for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) {
         pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag);
     }
 }
 
 /*!
  * \internal
  * \brief Add an XML attribute to a node, marked as deleted
  *
  * When calculating XML changes, we need to know when an attribute has been
  * deleted. Add the attribute back to the new XML, so that we can check the
  * removal against ACLs, and mark it as deleted for later removal after
  * differences have been calculated.
  *
  * \param[in,out] new_xml     XML to modify
  * \param[in]     element     Name of XML element that changed (for logging)
  * \param[in]     attr_name   Name of attribute that was deleted
  * \param[in]     old_value   Value of attribute that was deleted
  */
 static void
 mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name,
                   const char *old_value)
 {
     xml_doc_private_t *docpriv = new_xml->doc->_private;
     xmlAttr *attr = NULL;
     xml_node_private_t *nodepriv;
 
     // Prevent the dirty flag being set recursively upwards
     pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking);
 
     // Restore the old value (and the tracking flag)
     attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value);
     pcmk__set_xml_flags(docpriv, pcmk__xf_tracking);
 
     // Reset flags (so the attribute doesn't appear as newly created)
     nodepriv = attr->_private;
     nodepriv->flags = 0;
 
     // Check ACLs and mark restored value for later removal
     remove_xe_attr(new_xml, attr);
 
     crm_trace("XML attribute %s=%s was removed from %s",
               attr_name, old_value, element);
 }
 
 /*
  * \internal
  * \brief Check ACLs for a changed XML attribute
  */
 static void
 mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name,
                   const char *old_value)
 {
     char *vcopy = crm_element_value_copy(new_xml, attr_name);
 
     crm_trace("XML attribute %s was changed from '%s' to '%s' in %s",
               attr_name, old_value, vcopy, element);
 
     // Restore the original value
     xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value);
 
     // Change it back to the new value, to check ACLs
     crm_xml_add(new_xml, attr_name, vcopy);
     free(vcopy);
 }
 
 /*!
  * \internal
  * \brief Mark an XML attribute as having changed position
  *
  * \param[in,out] new_xml     XML to modify
  * \param[in]     element     Name of XML element that changed (for logging)
  * \param[in,out] old_attr    Attribute that moved, in original XML
  * \param[in,out] new_attr    Attribute that moved, in \p new_xml
  * \param[in]     p_old       Ordinal position of \p old_attr in original XML
  * \param[in]     p_new       Ordinal position of \p new_attr in \p new_xml
  */
 static void
 mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr,
                 xmlAttr *new_attr, int p_old, int p_new)
 {
     xml_node_private_t *nodepriv = new_attr->_private;
 
     crm_trace("XML attribute %s moved from position %d to %d in %s",
               old_attr->name, p_old, p_new, element);
 
     // Mark document, element, and all element's parents as changed
     pcmk__mark_xml_node_dirty(new_xml);
 
     // Mark attribute as changed
     pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved);
 
     nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private;
     pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
 }
 
 /*!
  * \internal
  * \brief Calculate differences in all previously existing XML attributes
  *
  * \param[in,out] old_xml  Original XML to compare
  * \param[in,out] new_xml  New XML to compare
  */
 static void
 xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml)
 {
     xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml);
 
     while (attr_iter != NULL) {
         const char *name = (const char *) attr_iter->name;
         xmlAttr *old_attr = attr_iter;
         xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name);
         const char *old_value = pcmk__xml_attr_value(attr_iter);
 
         attr_iter = attr_iter->next;
         if (new_attr == NULL) {
             mark_attr_deleted(new_xml, (const char *) old_xml->name, name,
                               old_value);
 
         } else {
             xml_node_private_t *nodepriv = new_attr->_private;
             int new_pos = pcmk__xml_position((xmlNode*) new_attr,
                                              pcmk__xf_skip);
             int old_pos = pcmk__xml_position((xmlNode*) old_attr,
                                              pcmk__xf_skip);
             const char *new_value = crm_element_value(new_xml, name);
 
             // This attribute isn't new
             pcmk__clear_xml_flags(nodepriv, pcmk__xf_created);
 
             if (strcmp(new_value, old_value) != 0) {
                 mark_attr_changed(new_xml, (const char *) old_xml->name, name,
                                   old_value);
 
             } else if ((old_pos != new_pos)
                        && !pcmk__tracking_xml_changes(new_xml, TRUE)) {
                 mark_attr_moved(new_xml, (const char *) old_xml->name,
                                 old_attr, new_attr, old_pos, new_pos);
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief Check all attributes in new XML for creation
  *
  * For each of a given XML element's attributes marked as newly created, accept
  * (and mark as dirty) or reject the creation according to ACLs.
  *
  * \param[in,out] new_xml  XML to check
  */
 static void
 mark_created_attrs(xmlNode *new_xml)
 {
     xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml);
 
     while (attr_iter != NULL) {
         xmlAttr *new_attr = attr_iter;
         xml_node_private_t *nodepriv = attr_iter->_private;
 
         attr_iter = attr_iter->next;
         if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
             const char *attr_name = (const char *) new_attr->name;
 
             crm_trace("Created new attribute %s=%s in %s",
                       attr_name, pcmk__xml_attr_value(new_attr),
                       new_xml->name);
 
             /* Check ACLs (we can't use the remove-then-create trick because it
              * would modify the attribute position).
              */
             if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) {
                 pcmk__mark_xml_attr_dirty(new_attr);
             } else {
                 // Creation was not allowed, so remove the attribute
                 xmlUnsetProp(new_xml, new_attr->name);
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief Calculate differences in attributes between two XML nodes
  *
  * \param[in,out] old_xml  Original XML to compare
  * \param[in,out] new_xml  New XML to compare
  */
 static void
 xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml)
 {
     set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new
     xml_diff_old_attrs(old_xml, new_xml);
     mark_created_attrs(new_xml);
 }
 
 /*!
  * \internal
  * \brief Add an XML child element to a node, marked as deleted
  *
  * When calculating XML changes, we need to know when a child element has been
  * deleted. Add the child back to the new XML, so that we can check the removal
  * against ACLs, and mark it as deleted for later removal after differences have
  * been calculated.
  *
  * \param[in,out] old_child    Child element from original XML
  * \param[in,out] new_parent   New XML to add marked copy to
  */
 static void
 mark_child_deleted(xmlNode *old_child, xmlNode *new_parent)
 {
     // Re-create the child element so we can check ACLs
     xmlNode *candidate = pcmk__xml_copy(new_parent, old_child);
 
     // Clear flags on new child and its children
     pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL);
 
     // Check whether ACLs allow the deletion
     pcmk__apply_acl(xmlDocGetRootElement(candidate->doc));
 
     // Remove the child again (which will track it in document's deleted_objs)
     free_xml_with_position(candidate,
                            pcmk__xml_position(old_child, pcmk__xf_skip));
 
     if (pcmk__xml_match(new_parent, old_child, true) == NULL) {
         pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private),
                             pcmk__xf_skip);
     }
 }
 
 static void
 mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child,
                  int p_old, int p_new)
 {
     xml_node_private_t *nodepriv = new_child->_private;
 
     crm_trace("Child element %s with "
               PCMK_XA_ID "='%s' moved from position %d to %d under %s",
               new_child->name, pcmk__s(pcmk__xe_id(new_child), "<no id>"),
               p_old, p_new, new_parent->name);
     pcmk__mark_xml_node_dirty(new_parent);
     pcmk__set_xml_flags(nodepriv, pcmk__xf_moved);
 
     if (p_old > p_new) {
         nodepriv = old_child->_private;
     } else {
         nodepriv = new_child->_private;
     }
     pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
 }
 
 // Given original and new XML, mark new XML portions that have changed
 static void
 mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top)
 {
     xmlNode *old_child = NULL;
     xmlNode *new_child = NULL;
     xml_node_private_t *nodepriv = NULL;
 
     CRM_CHECK(new_xml != NULL, return);
     if (old_xml == NULL) {
         pcmk__xml_mark_created(new_xml);
         pcmk__apply_creation_acl(new_xml, check_top);
         return;
     }
 
     nodepriv = new_xml->_private;
     CRM_CHECK(nodepriv != NULL, return);
 
     if(nodepriv->flags & pcmk__xf_processed) {
         /* Avoid re-comparing nodes */
         return;
     }
     pcmk__set_xml_flags(nodepriv, pcmk__xf_processed);
 
     xml_diff_attrs(old_xml, new_xml);
 
     // Check for differences in the original children
     for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL;
          old_child = pcmk__xml_next(old_child)) {
 
         new_child = pcmk__xml_match(new_xml, old_child, true);
 
         if (new_child != NULL) {
             mark_xml_changes(old_child, new_child, true);
 
         } else {
             mark_child_deleted(old_child, new_xml);
         }
     }
 
     // Check for moved or created children
     new_child = pcmk__xml_first_child(new_xml);
     while (new_child != NULL) {
         xmlNode *next = pcmk__xml_next(new_child);
 
         old_child = pcmk__xml_match(old_xml, new_child, true);
 
         if (old_child == NULL) {
             // This is a newly created child
             nodepriv = new_child->_private;
             pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
 
             // May free new_child
             mark_xml_changes(old_child, new_child, true);
 
         } else {
             /* Check for movement, we already checked for differences */
             int p_new = pcmk__xml_position(new_child, pcmk__xf_skip);
             int p_old = pcmk__xml_position(old_child, pcmk__xf_skip);
 
             if(p_old != p_new) {
                 mark_child_moved(old_child, new_xml, new_child, p_old, p_new);
             }
         }
 
         new_child = next;
     }
 }
 
 void
 xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml)
 {
     pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy);
     xml_calculate_changes(old_xml, new_xml);
 }
 
 // Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml
 void
 xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml)
 {
     CRM_CHECK((old_xml != NULL) && (new_xml != NULL)
               && pcmk__xe_is(old_xml, (const char *) new_xml->name)
               && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
                               pcmk__str_none),
               return);
 
     if(xml_tracking_changes(new_xml) == FALSE) {
         xml_track_changes(new_xml, NULL, NULL, FALSE);
     }
 
     mark_xml_changes(old_xml, new_xml, FALSE);
 }
 
 /*!
  * \internal
  * \brief Find a comment with matching content in specified XML
  *
  * \param[in] root            XML to search
  * \param[in] search_comment  Comment whose content should be searched for
  * \param[in] exact           If true, comment must also be at same position
  */
 xmlNode *
 pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact)
 {
     xmlNode *a_child = NULL;
     int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip);
 
     CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL);
 
     for (a_child = pcmk__xml_first_child(root); a_child != NULL;
          a_child = pcmk__xml_next(a_child)) {
         if (exact) {
             int offset = pcmk__xml_position(a_child, pcmk__xf_skip);
             xml_node_private_t *nodepriv = a_child->_private;
 
             if (offset < search_offset) {
                 continue;
 
             } else if (offset > search_offset) {
                 return NULL;
             }
 
             if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) {
                 continue;
             }
         }
 
         if (a_child->type == XML_COMMENT_NODE
             && pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) {
             return a_child;
 
         } else if (exact) {
             return NULL;
         }
     }
 
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Make one XML comment match another (in content)
  *
  * \param[in,out] parent   If \p target is NULL and this is not, add or update
  *                         comment child of this XML node that matches \p update
  * \param[in,out] target   If not NULL, update this XML comment node
  * \param[in]     update   Make comment content match this (must not be NULL)
  *
  * \note At least one of \parent and \target must be non-NULL
  */
 void
 pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update)
 {
     CRM_CHECK(update != NULL, return);
     CRM_CHECK(update->type == XML_COMMENT_NODE, return);
 
     if (target == NULL) {
         target = pcmk__xc_match(parent, update, false);
     }
 
     if (target == NULL) {
         pcmk__xml_copy(parent, update);
 
     } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) {
         xmlFree(target->content);
         target->content = xmlStrdup(update->content);
     }
 }
 
 /*!
  * \internal
  * \brief Merge one XML tree into another
  *
  * Here, "merge" means:
  * 1. Copy attribute values from \p update to the target, overwriting in case of
  *    conflict.
  * 2. Descend through \p update and the target in parallel. At each level, for
  *    each child of \p update, look for a matching child of the target.
  *    a. For each child, if a match is found, go to step 1, recursively merging
  *       the child of \p update into the child of the target.
  *    b. Otherwise, copy the child of \p update as a child of the target.
  *
  * A match is defined as the first child of the same type within the target,
  * with:
  * * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise,
  * * the \c PCMK_XA_ID_REF attribute matching, if set in \p update
  *
  * This function does not delete any elements or attributes from the target. It
  * may add elements or overwrite attributes, as described above.
  *
  * \param[in,out] parent   If \p target is NULL and this is not, add or update
  *                         child of this XML node that matches \p update
  * \param[in,out] target   If not NULL, update this XML
  * \param[in]     update   Make the desired XML match this (must not be \c NULL)
  * \param[in]     flags    Group of <tt>enum pcmk__xa_flags</tt>
  * \param[in]     as_diff  If \c true, preserve order of attributes (deprecated
  *                         since 2.0.5)
  *
  * \note At least one of \p parent and \p target must be non-<tt>NULL</tt>.
  * \note This function is recursive. For the top-level call, \p parent is
  *       \c NULL and \p target is not \c NULL. For recursive calls, \p target is
  *       \c NULL and \p parent is not \c NULL.
  */
 void
 pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update,
                  uint32_t flags, bool as_diff)
 {
     /* @COMPAT Refactor further and staticize after v1 patchset deprecation.
      *
      * @COMPAT Drop as_diff argument when apply_xml_diff() is dropped.
      */
     const char *update_name = NULL;
     const char *update_id_attr = NULL;
     const char *update_id_val = NULL;
     char *trace_s = NULL;
 
     crm_log_xml_trace(update, "update");
     crm_log_xml_trace(target, "target");
 
     CRM_CHECK(update != NULL, goto done);
 
     if (update->type == XML_COMMENT_NODE) {
         pcmk__xc_update(parent, target, update);
         goto done;
     }
 
     update_name = (const char *) update->name;
 
     CRM_CHECK(update_name != NULL, goto done);
     CRM_CHECK((target != NULL) || (parent != NULL), goto done);
 
     update_id_val = pcmk__xe_id(update);
     if (update_id_val != NULL) {
         update_id_attr = PCMK_XA_ID;
 
     } else {
         update_id_val = crm_element_value(update, PCMK_XA_ID_REF);
         if (update_id_val != NULL) {
             update_id_attr = PCMK_XA_ID_REF;
         }
     }
 
     pcmk__if_tracing(
         {
             if (update_id_attr != NULL) {
                 trace_s = crm_strdup_printf("<%s %s=%s/>",
                                             update_name, update_id_attr,
                                             update_id_val);
             } else {
                 trace_s = crm_strdup_printf("<%s/>", update_name);
             }
         },
         {}
     );
 
     if (target == NULL) {
         // Recursive call
         target = pcmk__xe_first_child(parent, update_name, update_id_attr,
                                       update_id_val);
     }
 
     if (target == NULL) {
         // Recursive call with no existing matching child
         target = pcmk__xe_create(parent, update_name);
         crm_trace("Added %s", pcmk__s(trace_s, update_name));
 
     } else {
         // Either recursive call with match, or top-level call
         crm_trace("Found node %s to update", pcmk__s(trace_s, update_name));
     }
 
     CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return);
 
     if (!as_diff) {
         pcmk__xe_copy_attrs(target, update, flags);
 
     } else {
         // Preserve order of attributes. Don't use pcmk__xe_copy_attrs().
         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 (xmlNode *child = pcmk__xml_first_child(update); child != NULL;
          child = pcmk__xml_next(child)) {
 
         crm_trace("Updating child of %s", pcmk__s(trace_s, update_name));
         pcmk__xml_update(target, NULL, child, flags, as_diff);
     }
 
     crm_trace("Finished with %s", pcmk__s(trace_s, update_name));
 
 done:
     free(trace_s);
 }
 
 /*!
  * \internal
  * \brief Delete an XML subtree if it matches a search element
  *
  * A match is defined as follows:
  * * \p xml and \p user_data are both element nodes of the same type.
  * * If \p user_data has attributes set, \p xml has those attributes set to the
  *   same values. (\p xml may have additional attributes set to arbitrary
  *   values.)
  *
  * \param[in,out] xml        XML subtree to delete upon match
  * \param[in]     user_data  Search element
  *
  * \return \c true to continue traversing the tree, or \c false to stop (because
  *         \p xml was deleted)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 delete_xe_if_matching(xmlNode *xml, void *user_data)
 {
     xmlNode *search = user_data;
 
     if (!pcmk__xe_is(search, (const char *) xml->name)) {
         // No match: either not both elements, or different element types
         return true;
     }
 
     for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL;
          attr = attr->next) {
 
         const char *search_val = pcmk__xml_attr_value(attr);
         const char *xml_val = crm_element_value(xml, (const char *) attr->name);
 
         if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) {
             // No match: an attr in xml doesn't match the attr in search
             return true;
         }
     }
 
     crm_log_xml_trace(xml, "delete-match");
     crm_log_xml_trace(search, "delete-search");
     pcmk__xml_free(xml);
 
     // Found a match and deleted it; stop traversing tree
     return false;
 }
 
 /*!
  * \internal
  * \brief Search an XML tree depth-first and delete the first matching element
  *
  * This function does not attempt to match the tree root (\p xml).
  *
  * A match with a node \c node is defined as follows:
  * * \c node and \p search are both element nodes of the same type.
  * * If \p search has attributes set, \c node has those attributes set to the
  *   same values. (\c node may have additional attributes set to arbitrary
  *   values.)
  *
  * \param[in,out] xml     XML subtree to search
  * \param[in]     search  Element to match against
  *
  * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
  *         successful deletion and an error code otherwise)
  */
 int
 pcmk__xe_delete_match(xmlNode *xml, xmlNode *search)
 {
     // See @COMPAT comment in pcmk__xe_replace_match()
     CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL);
 
     for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
          xml = pcmk__xe_next(xml)) {
 
         if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) {
             // Found and deleted an element
             return pcmk_rc_ok;
         }
     }
 
     // No match found in this subtree
     return ENXIO;
 }
 
 /*!
  * \internal
  * \brief Replace one XML node with a copy of another XML node
  *
  * This function handles change tracking and applies ACLs.
  *
  * \param[in,out] old  XML node to replace
  * \param[in]     new  XML node to copy as replacement for \p old
  *
  * \note This frees \p old.
  */
 static void
 replace_node(xmlNode *old, xmlNode *new)
 {
     new = xmlCopyNode(new, 1);
     pcmk__mem_assert(new);
 
     // May be unnecessary but avoids slight changes to some test outputs
     pcmk__xml_tree_foreach(new, reset_xml_node_flags, NULL);
 
     old = xmlReplaceNode(old, new);
 
     if (xml_tracking_changes(new)) {
         // Replaced sections may have included relevant ACLs
         pcmk__apply_acl(new);
     }
     xml_calculate_changes(old, new);
     xmlFreeNode(old);
 }
 
 /*!
  * \internal
  * \brief Replace one XML subtree with a copy of another if the two match
  *
  * A match is defined as follows:
  * * \p xml and \p user_data are both element nodes of the same type.
  * * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has
  *   \c PCMK_XA_ID set to the same value.
  *
  * \param[in,out] xml        XML subtree to replace with \p user_data upon match
  * \param[in]     user_data  XML to replace \p xml with a copy of upon match
  *
  * \return \c true to continue traversing the tree, or \c false to stop (because
  *         \p xml was replaced by \p user_data)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 replace_xe_if_matching(xmlNode *xml, void *user_data)
 {
     xmlNode *replace = user_data;
     const char *xml_id = NULL;
     const char *replace_id = NULL;
 
     xml_id = pcmk__xe_id(xml);
     replace_id = pcmk__xe_id(replace);
 
     if (!pcmk__xe_is(replace, (const char *) xml->name)) {
         // No match: either not both elements, or different element types
         return true;
     }
 
     if ((replace_id != NULL)
         && !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) {
 
         // No match: ID was provided in replace and doesn't match xml's ID
         return true;
     }
 
     crm_log_xml_trace(xml, "replace-match");
     crm_log_xml_trace(replace, "replace-with");
     replace_node(xml, replace);
 
     // Found a match and replaced it; stop traversing tree
     return false;
 }
 
 /*!
  * \internal
  * \brief Search an XML tree depth-first and replace the first matching element
  *
  * This function does not attempt to match the tree root (\p xml).
  *
  * A match with a node \c node is defined as follows:
  * * \c node and \p replace are both element nodes of the same type.
  * * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has
  *   \c PCMK_XA_ID set to the same value.
  *
  * \param[in,out] xml      XML tree to search
  * \param[in]     replace  XML to replace a matching element with a copy of
  *
  * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
  *         successful replacement and an error code otherwise)
  */
 int
 pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace)
 {
     /* @COMPAT Some of this behavior (like not matching the tree root, which is
      * allowed by pcmk__xe_update_match()) is questionable for general use but
      * required for backward compatibility by cib_process_replace() and
      * cib_process_delete(). Behavior can change at a major version release if
      * desired.
      */
     CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL);
 
     for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
          xml = pcmk__xe_next(xml)) {
 
         if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) {
             // Found and replaced an element
             return pcmk_rc_ok;
         }
     }
 
     // No match found in this subtree
     return ENXIO;
 }
 
 //! User data for \c update_xe_if_matching()
 struct update_data {
     xmlNode *update;    //!< Update source
     uint32_t flags;     //!< Group of <tt>enum pcmk__xa_flags</tt>
 };
 
 /*!
  * \internal
  * \brief Update one XML subtree with another if the two match
  *
  * "Update" means to merge a source subtree into a target subtree (see
  * \c pcmk__xml_update()).
  *
  * A match is defined as follows:
  * * \p xml and \p user_data->update are both element nodes of the same type.
  * * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute
  *   value, or \c PCMK_XA_ID is unset in both
  *
  * \param[in,out] xml        XML subtree to update with \p user_data->update
  *                           upon match
  * \param[in]     user_data  <tt>struct update_data</tt> object
  *
  * \return \c true to continue traversing the tree, or \c false to stop (because
  *         \p xml was updated by \p user_data->update)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 update_xe_if_matching(xmlNode *xml, void *user_data)
 {
     struct update_data *data = user_data;
     xmlNode *update = data->update;
 
     if (!pcmk__xe_is(update, (const char *) xml->name)) {
         // No match: either not both elements, or different element types
         return true;
     }
 
     if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) {
         // No match: ID mismatch
         return true;
     }
 
     crm_log_xml_trace(xml, "update-match");
     crm_log_xml_trace(update, "update-with");
     pcmk__xml_update(NULL, xml, update, data->flags, false);
 
     // Found a match and replaced it; stop traversing tree
     return false;
 }
 
 /*!
  * \internal
  * \brief Search an XML tree depth-first and update the first matching element
  *
  * "Update" means to merge a source subtree into a target subtree (see
  * \c pcmk__xml_update()).
  *
  * A match with a node \c node is defined as follows:
  * * \c node and \p update are both element nodes of the same type.
  * * \c node and \p update have the same \c PCMK_XA_ID attribute value, or
  *   \c PCMK_XA_ID is unset in both
  *
  * \param[in,out] xml     XML tree to search
  * \param[in]     update  XML to update a matching element with
  * \param[in]     flags   Group of <tt>enum pcmk__xa_flags</tt>
  *
  * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
  *         successful update and an error code otherwise)
  */
 int
 pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags)
 {
     /* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we
      * compare IDs only if the equivalent of the update argument has an ID.
      * Here, we're stricter: we consider it a mismatch if only one element has
      * an ID attribute, or if both elements have IDs but they don't match.
      *
      * Perhaps we should align the behavior at a major version release.
      */
     struct update_data data = {
         .update = update,
         .flags = flags,
     };
 
     CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL);
 
     if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) {
         // Found and updated an element
         return pcmk_rc_ok;
     }
 
     // No match found in this subtree
     return ENXIO;
 }
 
 xmlNode *
 sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive)
 {
     xmlNode *child = NULL;
     GSList *nvpairs = NULL;
     xmlNode *result = NULL;
 
     CRM_CHECK(input != NULL, return NULL);
 
     result = pcmk__xe_create(parent, (const char *) input->name);
     nvpairs = pcmk_xml_attrs2nvpairs(input);
     nvpairs = pcmk_sort_nvpairs(nvpairs);
     pcmk_nvpairs2xml_attrs(nvpairs, result);
     pcmk_free_nvpairs(nvpairs);
 
     for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL;
          child = pcmk__xe_next(child)) {
 
         if (recursive) {
             sorted_xml(child, result, recursive);
         } else {
             pcmk__xml_copy(result, child);
         }
     }
 
     return result;
 }
 
 /*!
  * \internal
  * \brief Get next sibling XML element with the same name as a given element
  *
  * \param[in] node  XML element to start from
  *
  * \return Next sibling XML element with same name
  */
 xmlNode *
 pcmk__xe_next_same(const xmlNode *node)
 {
     for (xmlNode *match = pcmk__xe_next(node); match != NULL;
          match = pcmk__xe_next(match)) {
 
         if (pcmk__xe_is(match, (const char *) node->name)) {
             return match;
         }
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Initialize the Pacemaker XML environment
  *
  * Set an XML buffer allocation scheme, set XML node create and destroy
  * callbacks, and load schemas into the cache.
  */
 void
 pcmk__xml_init(void)
 {
     // @TODO Try to find a better caller than crm_log_preinit()
     static bool initialized = false;
 
     if (!initialized) {
         initialized = true;
 
         /* Double the buffer size when the buffer needs to grow. The default
          * allocator XML_BUFFER_ALLOC_EXACT was found to cause poor performance
          * due to the number of reallocs.
          */
         xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT);
 
         // Initialize private data at node creation
         xmlRegisterNodeDefault(new_private_data);
 
         // Free private data at node destruction
         xmlDeregisterNodeDefault(free_private_data);
 
         // Load schemas into the cache
         pcmk__schema_init();
     }
 }
 
 /*!
  * \internal
  * \brief Tear down the Pacemaker XML environment
  *
  * Destroy schema cache and clean up memory allocated by libxml2.
  */
 void
 pcmk__xml_cleanup(void)
 {
     pcmk__schema_cleanup();
     xmlCleanupParser();
 }
 
 /*!
  * \internal
  * \brief Get the XML element whose \c PCMK_XA_ID matches an \c PCMK_XA_ID_REF
  *
  * \param[in] xml     Element whose \c PCMK_XA_ID_REF attribute to check
  * \param[in] search  Node whose document to search for node with matching
  *                    \c PCMK_XA_ID (\c NULL to use \p xml)
  *
  * \return If \p xml has a \c PCMK_XA_ID_REF attribute, node in
  *         <tt>search</tt>'s document whose \c PCMK_XA_ID attribute matches;
  *         otherwise, \p xml
  */
 xmlNode *
 pcmk__xe_resolve_idref(xmlNode *xml, xmlNode *search)
 {
     char *xpath = NULL;
     const char *ref = NULL;
     xmlNode *result = NULL;
 
     if (xml == NULL) {
         return NULL;
     }
 
     ref = crm_element_value(xml, PCMK_XA_ID_REF);
     if (ref == NULL) {
         return xml;
     }
 
     if (search == NULL) {
         search = xml;
     }
 
     xpath = crm_strdup_printf("//%s[@" PCMK_XA_ID "='%s']", xml->name, ref);
     result = get_xpath_object(xpath, search, 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",
                          xml->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();
 
         free(ret);
         ret = find_artefact(ns, remote_schema_dir, filespec);
     }
 
     return ret;
 }
 
 void
 pcmk__xe_set_propv(xmlNodePtr node, va_list pairs)
 {
     while (true) {
         const char *name, *value;
 
         name = va_arg(pairs, const char *);
         if (name == NULL) {
             return;
         }
 
         value = va_arg(pairs, const char *);
         if (value != NULL) {
             crm_xml_add(node, name, value);
         }
     }
 }
 
 void
 pcmk__xe_set_props(xmlNodePtr node, ...)
 {
     va_list pairs;
     va_start(pairs, node);
     pcmk__xe_set_propv(node, pairs);
     va_end(pairs);
 }
 
 int
 pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
                        int (*handler)(xmlNode *xml, void *userdata),
                        void *userdata)
 {
     xmlNode *children = (xml? xml->children : NULL);
 
     CRM_ASSERT(handler != NULL);
 
     for (xmlNode *node = children; node != NULL; node = node->next) {
         if ((node->type == XML_ELEMENT_NODE)
             && ((child_element_name == NULL)
                 || pcmk__xe_is(node, child_element_name))) {
             int rc = handler(node, userdata);
 
             if (rc != pcmk_rc_ok) {
                 return rc;
             }
         }
     }
 
     return pcmk_rc_ok;
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/common/xml_compat.h>
 
-xmlNode *
-find_entity(xmlNode *parent, const char *node_name, const char *id)
-{
-    return pcmk__xe_first_child(parent, node_name,
-                                ((id == NULL)? id : PCMK_XA_ID), id);
-}
-
-void
-crm_destroy_xml(gpointer data)
-{
-    pcmk__xml_free(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__xml_mark_created(child);
-    return child;
-}
-
-int
-add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child)
-{
-    add_node_copy(parent, child);
-    pcmk__xml_free(child);
-    return 1;
-}
-
-gboolean
-xml_has_children(const xmlNode * xml_root)
-{
-    if (xml_root != NULL && xml_root->children != NULL) {
-        return TRUE;
-    }
-    return FALSE;
-}
-
-static char *
-replace_text(char *text, size_t *index, size_t *length, const char *replace)
-{
-    // 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;
-}
-
-char *
-crm_xml_escape(const char *text)
-{
-    size_t length = 0;
-    char *copy = NULL;
-
-    if (text == NULL) {
-        return NULL;
-    }
-
-    length = strlen(text);
-    copy = pcmk__str_copy(text);
-    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, "&lt;");
-                break;
-            case '>':
-                copy = replace_text(copy, &index, &length, "&gt;");
-                break;
-            case '"':
-                copy = replace_text(copy, &index, &length, "&quot;");
-                break;
-            case '\'':
-                copy = replace_text(copy, &index, &length, "&apos;");
-                break;
-            case '&':
-                copy = replace_text(copy, &index, &length, "&amp;");
-                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 = NULL;
 
     pcmk__mem_assert(doc);
 
     copy = xmlDocCopyNode(src, doc, 1);
     pcmk__mem_assert(copy);
 
     xmlDocSetRootElement(doc, copy);
     return copy;
 }
 
-xmlNode *
-create_xml_node(xmlNode *parent, const char *name)
-{
-    // Like pcmk__xe_create(), but returns NULL on failure
-    xmlNode *node = NULL;
-
-    CRM_CHECK(!pcmk__str_empty(name), return NULL);
-
-    if (parent == NULL) {
-        xmlDoc *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__xml_mark_created(node);
-    return node;
-}
-
-xmlNode *
-pcmk_create_xml_text_node(xmlNode *parent, const char *name,
-                          const char *content)
-{
-    xmlNode *node = pcmk__xe_create(parent, name);
-
-    pcmk__xe_set_content(node, "%s", 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__html_create(parent, element_name, id, class_name);
-
-    pcmk__xe_set_content(node, "%s", text);
-    return node;
-}
-
-xmlNode *
-first_named_child(const xmlNode *parent, const char *name)
-{
-    return pcmk__xe_first_child(parent, name, NULL, NULL);
-}
-
-xmlNode *
-find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find)
-{
-    xmlNode *result = NULL;
-
-    if (search_path == NULL) {
-        crm_warn("Will never find <NULL>");
-        return NULL;
-    }
-
-    result = pcmk__xe_first_child(root, search_path, NULL, NULL);
-
-    if (must_find && (result == NULL)) {
-        crm_warn("Could not find %s in %s",
-                 search_path,
-                 ((root != NULL)? (const char *) root->name : "<NULL>"));
-    }
-
-    return result;
-}
-
-xmlNode *
-crm_next_same_xml(const xmlNode *sibling)
-{
-    return pcmk__xe_next_same(sibling);
-}
-
-void
-xml_remove_prop(xmlNode * obj, const char *name)
-{
-    pcmk__xe_remove_attr(obj, name);
-}
-
-gboolean
-replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only)
-{
-    bool is_match = false;
-    const char *child_id = NULL;
-    const char *update_id = NULL;
-
-    CRM_CHECK(child != NULL, return FALSE);
-    CRM_CHECK(update != NULL, return FALSE);
-
-    child_id = pcmk__xe_id(child);
-    update_id = pcmk__xe_id(update);
-
-    /* Match element name and (if provided in update XML) element ID. Don't
-     * match search root (child is search root if parent == NULL).
-     */
-    is_match = (parent != NULL)
-               && pcmk__xe_is(update, (const char *) child->name)
-               && ((update_id == NULL)
-                   || pcmk__str_eq(update_id, child_id, pcmk__str_none));
-
-    /* For deletion, match all attributes provided in update. A matching node
-     * can have additional attributes, but values must match for provided ones.
-     */
-    if (is_match && delete_only) {
-        for (xmlAttr *attr = pcmk__xe_first_attr(update); attr != NULL;
-             attr = attr->next) {
-            const char *name = (const char *) attr->name;
-            const char *update_val = pcmk__xml_attr_value(attr);
-            const char *child_val = crm_element_value(child, name);
-
-            if (!pcmk__str_eq(update_val, child_val, pcmk__str_casei)) {
-                is_match = false;
-                break;
-            }
-        }
-    }
-
-    if (is_match) {
-        if (delete_only) {
-            crm_log_xml_trace(child, "delete-match");
-            crm_log_xml_trace(update, "delete-search");
-            pcmk__xml_free(child);
-
-        } else {
-            crm_log_xml_trace(child, "replace-match");
-            crm_log_xml_trace(update, "replace-with");
-            replace_node(child, update);
-        }
-        return TRUE;
-    }
-
-    // Current node not a match; search the rest of the subtree depth-first
-    parent = child;
-    for (child = pcmk__xml_first_child(parent); child != NULL;
-         child = pcmk__xml_next(child)) {
-
-        // Only delete/replace the first match
-        if (replace_xml_child(parent, child, update, delete_only)) {
-            return TRUE;
-        }
-    }
-
-    // No match found in this subtree
-    return FALSE;
-}
-
-gboolean
-update_xml_child(xmlNode *child, xmlNode *to_update)
-{
-    return pcmk__xe_update_match(child, to_update,
-                                 pcmk__xaf_score_update) == pcmk_rc_ok;
-}
-
-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 = pcmk__xe_create(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;
-}
-
-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__xe_first_child(target, NULL, NULL, NULL); child != NULL;
-         child = pcmk__xe_next(child)) {
-
-        fix_plus_plus_recursive(child);
-    }
-}
-
-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;
-            }
-        }
-    }
-}
-
-void
-expand_plus_plus(xmlNode * target, const char *name, const char *value)
-{
-    pcmk__xe_set_score(target, name, value);
-}
-
 void
 crm_xml_init(void)
 {
     pcmk__xml_init();
 }
 
 void
 crm_xml_cleanup(void)
 {
     pcmk__xml_cleanup();
 }
 
 void
 pcmk_free_xml_subtree(xmlNode *xml)
 {
     xmlUnlinkNode(xml); // Detaches from parent and siblings
     xmlFreeNode(xml);   // Frees
 }
 
 void
 free_xml(xmlNode *child)
 {
     pcmk__xml_free(child);
 }
 
 xmlNode *
 expand_idref(xmlNode *input, xmlNode *top)
 {
     return pcmk__xe_resolve_idref(input, top);
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/lib/common/xml_display.c b/lib/common/xml_display.c
index b563d3a6dd..502051ea8c 100644
--- a/lib/common/xml_display.c
+++ b/lib/common/xml_display.c
@@ -1,546 +1,401 @@
 /*
  * 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 <crm_internal.h>
 
 #include <libxml/tree.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>  // PCMK__XML_LOG_BASE, etc.
 #include "crmcommon_private.h"
 
 static int show_xml_node(pcmk__output_t *out, GString *buffer,
                          const char *prefix, const xmlNode *data, int depth,
                          uint32_t options);
 
 // Log an XML library error
 void
 pcmk__log_xmllib_err(void *ctx, const char *fmt, ...)
 {
     va_list ap;
 
     va_start(ap, fmt);
     pcmk__if_tracing(
         {
             PCMK__XML_LOG_BASE(LOG_ERR, TRUE,
                                crm_abort(__FILE__, __PRETTY_FUNCTION__,
                                          __LINE__, "xml library error", TRUE,
                                          TRUE),
                                "XML Error: ", fmt, ap);
         },
         {
             PCMK__XML_LOG_BASE(LOG_ERR, TRUE, 0, "XML Error: ", fmt, ap);
         }
     );
     va_end(ap);
 }
 
 /*!
  * \internal
  * \brief Output an XML comment with depth-based indentation
  *
  * \param[in,out] out      Output object
  * \param[in]     data     XML node to output
  * \param[in]     depth    Current indentation level
  * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
  *
  * \return Standard Pacemaker return code
  *
  * \note This currently produces output only for text-like output objects.
  */
 static int
 show_xml_comment(pcmk__output_t *out, const xmlNode *data, int depth,
                  uint32_t options)
 {
     if (pcmk_is_set(options, pcmk__xml_fmt_open)) {
         int width = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0;
 
         return out->info(out, "%*s<!--%s-->",
                          width, "", (const char *) data->content);
     }
     return pcmk_rc_no_output;
 }
 
 /*!
  * \internal
  * \brief Output an XML element in a formatted way
  *
  * \param[in,out] out      Output object
  * \param[in,out] buffer   Where to build output strings
  * \param[in]     prefix   String to prepend to every line of output
  * \param[in]     data     XML node to output
  * \param[in]     depth    Current indentation level
  * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
  *
  * \return Standard Pacemaker return code
  *
  * \note This is a recursive helper function for \p show_xml_node().
  * \note This currently produces output only for text-like output objects.
  * \note \p buffer may be overwritten many times. The caller is responsible for
  *       freeing it using \p g_string_free() but should not rely on its
  *       contents.
  */
 static int
 show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix,
                  const xmlNode *data, int depth, uint32_t options)
 {
     int spaces = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0;
     int rc = pcmk_rc_no_output;
 
     if (pcmk_is_set(options, pcmk__xml_fmt_open)) {
         const char *hidden = crm_element_value(data, PCMK__XA_HIDDEN);
 
         g_string_truncate(buffer, 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) {
             xml_node_private_t *nodepriv = attr->_private;
             const char *p_name = (const char *) attr->name;
             const char *p_value = pcmk__xml_attr_value(attr);
             gchar *p_copy = NULL;
 
             if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) {
                 continue;
             }
 
             // @COMPAT Remove when v1 patchsets are removed
             if (pcmk_any_flags_set(options,
                                    pcmk__xml_fmt_diff_plus
                                    |pcmk__xml_fmt_diff_minus)
                 && (strcmp(PCMK__XA_CRM_DIFF_MARKER, p_name) == 0)) {
                 continue;
             }
 
             if ((hidden != NULL) && (p_name[0] != '\0')
                 && (strstr(hidden, p_name) != NULL)) {
 
                 p_value = "*****";
 
             } else {
                 p_copy = pcmk__xml_escape(p_value, true);
                 p_value = p_copy;
             }
 
             pcmk__g_strcat(buffer, " ", p_name, "=\"",
                            pcmk__s(p_value, "<null>"), "\"", NULL);
             g_free(p_copy);
         }
 
         if ((data->children != NULL)
             && pcmk_is_set(options, pcmk__xml_fmt_children)) {
             g_string_append_c(buffer, '>');
 
         } else {
             g_string_append(buffer, "/>");
         }
 
         rc = out->info(out, "%s%s%s",
                        pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ",
                        buffer->str);
     }
 
     if (data->children == NULL) {
         return rc;
     }
 
     if (pcmk_is_set(options, pcmk__xml_fmt_children)) {
         for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL;
              child = pcmk__xml_next(child)) {
 
             int temp_rc = show_xml_node(out, buffer, prefix, child, depth + 1,
                                         options
                                         |pcmk__xml_fmt_open
                                         |pcmk__xml_fmt_close);
             rc = pcmk__output_select_rc(rc, temp_rc);
         }
     }
 
     if (pcmk_is_set(options, pcmk__xml_fmt_close)) {
         int temp_rc = out->info(out, "%s%s%*s</%s>",
                                 pcmk__s(prefix, ""),
                                 pcmk__str_empty(prefix)? "" : " ",
                                 spaces, "", data->name);
         rc = pcmk__output_select_rc(rc, temp_rc);
     }
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Output an XML element or comment in a formatted way
  *
  * \param[in,out] out      Output object
  * \param[in,out] buffer   Where to build output strings
  * \param[in]     prefix   String to prepend to every line of output
  * \param[in]     data     XML node to log
  * \param[in]     depth    Current indentation level
  * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
  *
  * \return Standard Pacemaker return code
  *
  * \note This is a recursive helper function for \p pcmk__xml_show().
  * \note This currently produces output only for text-like output objects.
  * \note \p buffer may be overwritten many times. The caller is responsible for
  *       freeing it using \p g_string_free() but should not rely on its
  *       contents.
  */
 static int
 show_xml_node(pcmk__output_t *out, GString *buffer, const char *prefix,
               const xmlNode *data, int depth, uint32_t options)
 {
     switch (data->type) {
         case XML_COMMENT_NODE:
             return show_xml_comment(out, data, depth, options);
         case XML_ELEMENT_NODE:
             return show_xml_element(out, buffer, prefix, data, depth, options);
         default:
             return pcmk_rc_no_output;
     }
 }
 
 /*!
  * \internal
  * \brief Output an XML element or comment in a formatted way
  *
  * \param[in,out] out        Output object
  * \param[in]     prefix     String to prepend to every line of output
  * \param[in]     data       XML node to output
  * \param[in]     depth      Current nesting level
  * \param[in]     options    Group of \p pcmk__xml_fmt_options flags
  *
  * \return Standard Pacemaker return code
  *
  * \note This currently produces output only for text-like output objects.
  */
 int
 pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data,
                int depth, uint32_t options)
 {
     int rc = pcmk_rc_no_output;
     GString *buffer = NULL;
 
     CRM_ASSERT(out != NULL);
     CRM_CHECK(depth >= 0, depth = 0);
 
     if (data == NULL) {
         return rc;
     }
 
     /* Allocate a buffer once, for show_xml_node() to truncate and reuse in
      * recursive calls
      */
     buffer = g_string_sized_new(1024);
     rc = show_xml_node(out, buffer, prefix, data, depth, options);
     g_string_free(buffer, TRUE);
 
     return rc;
 }
 
 /*!
  * \internal
  * \brief Output XML portions that have been marked as changed
  *
  * \param[in,out] out      Output object
  * \param[in]     data     XML node to output
  * \param[in]     depth    Current indentation level
  * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
  *
  * \note This is a recursive helper for \p pcmk__xml_show_changes(), showing
  *       changes to \p data and its children.
  * \note This currently produces output only for text-like output objects.
  */
 static int
 show_xml_changes_recursive(pcmk__output_t *out, const xmlNode *data, int depth,
                            uint32_t options)
 {
     /* @COMPAT: When log_data_element() is removed, we can remove the options
      * argument here and instead hard-code pcmk__xml_log_pretty.
      */
     xml_node_private_t *nodepriv = (xml_node_private_t *) data->_private;
     int rc = pcmk_rc_no_output;
     int temp_rc = pcmk_rc_no_output;
 
     if (pcmk_all_flags_set(nodepriv->flags, pcmk__xf_dirty|pcmk__xf_created)) {
         // Newly created
         return pcmk__xml_show(out, PCMK__XML_PREFIX_CREATED, data, depth,
                               options
                               |pcmk__xml_fmt_open
                               |pcmk__xml_fmt_children
                               |pcmk__xml_fmt_close);
     }
 
     if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) {
         // Modified or moved
         bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
         int spaces = pretty? (2 * depth) : 0;
         const char *prefix = PCMK__XML_PREFIX_MODIFIED;
 
         if (pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) {
             prefix = PCMK__XML_PREFIX_MOVED;
         }
 
         // Log opening tag
         rc = pcmk__xml_show(out, prefix, data, depth,
                             options|pcmk__xml_fmt_open);
 
         // Log changes to attributes
         for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL;
              attr = attr->next) {
             const char *name = (const char *) attr->name;
 
             nodepriv = attr->_private;
 
             if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) {
                 const char *value = pcmk__xml_attr_value(attr);
 
                 temp_rc = out->info(out, "%s %*s @%s=%s",
                                     PCMK__XML_PREFIX_DELETED, spaces, "", name,
                                     value);
 
             } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) {
                 const char *value = pcmk__xml_attr_value(attr);
 
                 if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
                     prefix = PCMK__XML_PREFIX_CREATED;
 
                 } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_modified)) {
                     prefix = PCMK__XML_PREFIX_MODIFIED;
 
                 } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) {
                     prefix = PCMK__XML_PREFIX_MOVED;
 
                 } else {
                     prefix = PCMK__XML_PREFIX_MODIFIED;
                 }
 
                 temp_rc = out->info(out, "%s %*s @%s=%s",
                                     prefix, spaces, "", name, value);
             }
             rc = pcmk__output_select_rc(rc, temp_rc);
         }
 
         // Log changes to children
         for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL;
              child = pcmk__xml_next(child)) {
             temp_rc = show_xml_changes_recursive(out, child, depth + 1,
                                                  options);
             rc = pcmk__output_select_rc(rc, temp_rc);
         }
 
         // Log closing tag
         temp_rc = pcmk__xml_show(out, PCMK__XML_PREFIX_MODIFIED, data, depth,
                                  options|pcmk__xml_fmt_close);
         return pcmk__output_select_rc(rc, temp_rc);
     }
 
     // This node hasn't changed, but check its children
     for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL;
          child = pcmk__xml_next(child)) {
         temp_rc = show_xml_changes_recursive(out, child, depth + 1, options);
         rc = pcmk__output_select_rc(rc, temp_rc);
     }
     return rc;
 }
 
 /*!
  * \internal
  * \brief Output changes to an XML node and any children
  *
  * \param[in,out] out  Output object
  * \param[in]     xml  XML node to output
  *
  * \return Standard Pacemaker return code
  *
  * \note This currently produces output only for text-like output objects.
  */
 int
 pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml)
 {
     xml_doc_private_t *docpriv = NULL;
     int rc = pcmk_rc_no_output;
     int temp_rc = pcmk_rc_no_output;
 
     CRM_ASSERT(out != NULL);
     CRM_ASSERT(xml != NULL);
     CRM_ASSERT(xml->doc != NULL);
 
     docpriv = xml->doc->_private;
     if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) {
         return rc;
     }
 
     for (const GList *iter = docpriv->deleted_objs; iter != NULL;
          iter = iter->next) {
         const pcmk__deleted_xml_t *deleted_obj = iter->data;
 
         if (deleted_obj->position >= 0) {
             temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s (%d)",
                                 deleted_obj->path, deleted_obj->position);
         } else {
             temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s",
                                 deleted_obj->path);
         }
         rc = pcmk__output_select_rc(rc, temp_rc);
     }
 
     temp_rc = show_xml_changes_recursive(out, xml, 0, pcmk__xml_fmt_pretty);
     return pcmk__output_select_rc(rc, temp_rc);
 }
-
-// Deprecated functions kept only for backward API compatibility
-// LCOV_EXCL_START
-
-#include <crm/common/logging_compat.h>
-#include <crm/common/xml_compat.h>
-
-void
-log_data_element(int log_level, const char *file, const char *function,
-                 int line, const char *prefix, const xmlNode *data, int depth,
-                 int legacy_options)
-{
-    uint32_t options = 0;
-    pcmk__output_t *out = NULL;
-
-    // Confine log_level to uint8_t range
-    log_level = pcmk__clip_log_level(log_level);
-
-    if (data == NULL) {
-        do_crm_log(log_level, "%s%sNo data to dump as XML",
-                   pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ");
-        return;
-    }
-
-    switch (log_level) {
-        case LOG_NEVER:
-            return;
-        case LOG_STDOUT:
-            CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return);
-            break;
-        default:
-            CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return);
-            pcmk__output_set_log_level(out, log_level);
-            break;
-    }
-
-    /* Map xml_log_options to pcmk__xml_fmt_options so that we can go ahead and
-     * start using the pcmk__xml_fmt_options in all the internal functions.
-     *
-     * xml_log_option_dirty_add and xml_log_option_diff_all are ignored by
-     * internal code and only used here, so they don't need to be addressed.
-     */
-    if (pcmk_is_set(legacy_options, xml_log_option_filtered)) {
-        options |= pcmk__xml_fmt_filtered;
-    }
-    if (pcmk_is_set(legacy_options, xml_log_option_formatted)) {
-        options |= pcmk__xml_fmt_pretty;
-    }
-    if (pcmk_is_set(legacy_options, xml_log_option_open)) {
-        options |= pcmk__xml_fmt_open;
-    }
-    if (pcmk_is_set(legacy_options, xml_log_option_children)) {
-        options |= pcmk__xml_fmt_children;
-    }
-    if (pcmk_is_set(legacy_options, xml_log_option_close)) {
-        options |= pcmk__xml_fmt_close;
-    }
-    if (pcmk_is_set(legacy_options, xml_log_option_text)) {
-        options |= pcmk__xml_fmt_text;
-    }
-    if (pcmk_is_set(legacy_options, xml_log_option_diff_plus)) {
-        options |= pcmk__xml_fmt_diff_plus;
-    }
-    if (pcmk_is_set(legacy_options, xml_log_option_diff_minus)) {
-        options |= pcmk__xml_fmt_diff_minus;
-    }
-    if (pcmk_is_set(legacy_options, xml_log_option_diff_short)) {
-        options |= pcmk__xml_fmt_diff_short;
-    }
-
-    // Log element based on options
-    if (pcmk_is_set(legacy_options, xml_log_option_dirty_add)) {
-        CRM_CHECK(depth >= 0, depth = 0);
-        show_xml_changes_recursive(out, data, depth, options);
-        goto done;
-    }
-
-    if (pcmk_is_set(options, pcmk__xml_fmt_pretty)
-        && ((data->children == NULL)
-            || (crm_element_value(data, PCMK__XA_CRM_DIFF_MARKER) != NULL))) {
-
-        if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) {
-            legacy_options |= xml_log_option_diff_all;
-            prefix = PCMK__XML_PREFIX_CREATED;
-
-        } else if (pcmk_is_set(options, pcmk__xml_fmt_diff_minus)) {
-            legacy_options |= xml_log_option_diff_all;
-            prefix = PCMK__XML_PREFIX_DELETED;
-        }
-    }
-
-    if (pcmk_is_set(options, pcmk__xml_fmt_diff_short)
-        && !pcmk_is_set(legacy_options, xml_log_option_diff_all)) {
-
-        if (!pcmk_any_flags_set(options,
-                                pcmk__xml_fmt_diff_plus
-                                |pcmk__xml_fmt_diff_minus)) {
-            // Nothing will ever be logged
-            goto done;
-        }
-
-        // Keep looking for the actual change
-        for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL;
-             child = pcmk__xml_next(child)) {
-            log_data_element(log_level, file, function, line, prefix, child,
-                             depth + 1, options);
-        }
-
-    } else {
-        pcmk__xml_show(out, prefix, data, depth,
-                       options
-                       |pcmk__xml_fmt_open
-                       |pcmk__xml_fmt_children
-                       |pcmk__xml_fmt_close);
-    }
-
-done:
-    out->finish(out, CRM_EX_OK, true, NULL);
-    pcmk__output_free(out);
-}
-
-void
-xml_log_changes(uint8_t log_level, const char *function, const xmlNode *xml)
-{
-    pcmk__output_t *out = NULL;
-    int rc = pcmk_rc_ok;
-
-    switch (log_level) {
-        case LOG_NEVER:
-            return;
-        case LOG_STDOUT:
-            CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return);
-            break;
-        default:
-            CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return);
-            pcmk__output_set_log_level(out, log_level);
-            break;
-    }
-    rc = pcmk__xml_show_changes(out, xml);
-    out->finish(out, pcmk_rc2exitc(rc), true, NULL);
-    pcmk__output_free(out);
-}
-
-// LCOV_EXCL_STOP
-// End deprecated API
diff --git a/lib/common/xml_io.c b/lib/common/xml_io.c
index f88e0b523e..b84395233a 100644
--- a/lib/common/xml_io.c
+++ b/lib/common/xml_io.c
@@ -1,840 +1,840 @@
 /*
  * 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 <crm_internal.h>
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/types.h>
 
 #include <bzlib.h>
 #include <libxml/parser.h>
 #include <libxml/tree.h>
 #include <libxml/xmlIO.h>               // xmlOutputBuffer*
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_io.h>
 #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 <tt>bzip2</tt>-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 (<tt>xmlDoc **</tt>)
  * \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) {
         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) {
         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__xml_string(child, options, buffer, depth + 1);
         }
 
         for (int lpc = 0; lpc < spaces; lpc++) {
             g_string_append_c(buffer, ' ');
         }
 
         pcmk__g_strcat(buffer, "</", data->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]     options  Group of <tt>enum pcmk__xml_fmt_options</tt>
  * \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;
     gchar *content_esc = NULL;
 
     if (pcmk__xml_needs_escape(content, pcmk__xml_escape_text)) {
         content_esc = pcmk__xml_escape(content, pcmk__xml_escape_text);
         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');
     }
     g_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, "<![CDATA[", (const char *) data->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, "<!--", (const char *) data->content, "-->", 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_type_text(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 string representation of an XML object
  *
  * libxml2's \c xmlNodeDumpOutput() doesn't allow filtering, doesn't escape
  * special characters thoroughly, and doesn't allow a const argument.
  *
  * \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
  *
  * \todo Create a wrapper that doesn't require \p depth. Only used with
  *       recursive calls currently.
  */
 void
 pcmk__xml_string(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_type_text(data->type), data->type);
             break;
     }
 }
 
 /*!
  * \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 rc = pcmk_rc_ok;
 
     // (5, 0, 0): (intermediate block size, silent, default workFactor)
     BZFILE *bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 0);
 
     rc = pcmk__bzlib2rc(rc);
     if (rc != pcmk_rc_ok) {
         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(&rc, bz_file, text, strlen(text));
     rc = pcmk__bzlib2rc(rc);
     if (rc != pcmk_rc_ok) {
         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(&rc, bz_file, 0, &bytes_in, bytes_out);
     bz_file = NULL;
     rc = pcmk__bzlib2rc(rc);
     if (rc != pcmk_rc_ok) {
         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(&rc, 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()
     GString *buffer = g_string_sized_new(1024);
     unsigned int bytes_out = 0;
     int rc = pcmk_rc_ok;
 
     pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0);
     CRM_CHECK(!pcmk__str_empty(buffer->str),
               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->str, filename, stream,
                                     &bytes_out) == pcmk_rc_ok)) {
         goto done;
     }
 
     rc = fprintf(stream, "%s", buffer->str);
     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_string_free(buffer, TRUE);
     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);
     pcmk__mem_assert(fd_out);
     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 <crm/common/xml_io_compat.h>
 
 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;
     GString *buffer = g_string_sized_new(1024);
 
     pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0);
 
     str = pcmk__str_copy(buffer->str);
     g_string_free(buffer, TRUE);
     return str;
 }
 
 char *
 dump_xml_formatted_with_text(const xmlNode *xml)
 {
     char *str = NULL;
     GString *buffer = g_string_sized_new(1024);
 
     pcmk__xml_string(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buffer, 0);
 
     str = pcmk__str_copy(buffer->str);
     g_string_free(buffer, TRUE);
     return str;
 }
 
 char *
 dump_xml_unformatted(const xmlNode *xml)
 {
     char *str = NULL;
     GString *buffer = g_string_sized_new(1024);
 
     pcmk__xml_string(xml, 0, buffer, 0);
 
     str = pcmk__str_copy(buffer->str);
     g_string_free(buffer, TRUE);
     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
diff --git a/lib/common/xpath.c b/lib/common/xpath.c
index 9fc95c5a5f..1cb47b5604 100644
--- a/lib/common/xpath.c
+++ b/lib/common/xpath.c
@@ -1,396 +1,342 @@
 /*
  * 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 <crm_internal.h>
 #include <stdio.h>
 #include <string.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include "crmcommon_private.h"
 
 /*
  * From xpath2.c
  *
  * All the elements returned by an XPath query are pointers to
  * elements from the tree *except* namespace nodes where the XPath
  * semantic is different from the implementation in libxml2 tree.
  * As a result when a returned node set is freed when
  * xmlXPathFreeObject() is called, that routine must check the
  * element type. But node from the returned set may have been removed
  * by xmlNodeSetContent() resulting in access to freed data.
  *
  * This can be exercised by running
  *       valgrind xpath2 test3.xml '//discarded' discarded
  *
  * There is 2 ways around it:
  *   - make a copy of the pointers to the nodes from the result set
  *     then call xmlXPathFreeObject() and then modify the nodes
  * or
  * - remove the references from the node set, if they are not
        namespace nodes, before calling xmlXPathFreeObject().
  */
 void
 freeXpathObject(xmlXPathObjectPtr xpathObj)
 {
     int lpc, max = numXpathResults(xpathObj);
 
     if (xpathObj == NULL) {
         return;
     }
 
     for (lpc = 0; lpc < max; lpc++) {
         if (xpathObj->nodesetval->nodeTab[lpc] && xpathObj->nodesetval->nodeTab[lpc]->type != XML_NAMESPACE_DECL) {
             xpathObj->nodesetval->nodeTab[lpc] = NULL;
         }
     }
 
     /* _Now_ it's safe to free it */
     xmlXPathFreeObject(xpathObj);
 }
 
 xmlNode *
 getXpathResult(xmlXPathObjectPtr xpathObj, int index)
 {
     xmlNode *match = NULL;
     int max = numXpathResults(xpathObj);
 
     CRM_CHECK(index >= 0, return NULL);
     CRM_CHECK(xpathObj != NULL, return NULL);
 
     if (index >= max) {
         crm_err("Requested index %d of only %d items", index, max);
         return NULL;
 
     } else if(xpathObj->nodesetval->nodeTab[index] == NULL) {
         /* Previously requested */
         return NULL;
     }
 
     match = xpathObj->nodesetval->nodeTab[index];
     CRM_CHECK(match != NULL, return NULL);
 
     if (xpathObj->nodesetval->nodeTab[index]->type != XML_NAMESPACE_DECL) {
         /* See the comment for freeXpathObject() */
         xpathObj->nodesetval->nodeTab[index] = NULL;
     }
 
     if (match->type == XML_DOCUMENT_NODE) {
         /* Will happen if section = '/' */
         match = match->children;
 
     } else if (match->type != XML_ELEMENT_NODE
                && match->parent && match->parent->type == XML_ELEMENT_NODE) {
         /* Return the parent instead */
         match = match->parent;
 
     } else if (match->type != XML_ELEMENT_NODE) {
         /* We only support searching nodes */
         crm_err("We only support %d not %d", XML_ELEMENT_NODE, match->type);
         match = NULL;
     }
     return match;
 }
 
 void
 dedupXpathResults(xmlXPathObjectPtr xpathObj)
 {
     int lpc, max = numXpathResults(xpathObj);
 
     if (xpathObj == NULL) {
         return;
     }
 
     for (lpc = 0; lpc < max; lpc++) {
         xmlNode *xml = NULL;
         gboolean dedup = FALSE;
 
         if (xpathObj->nodesetval->nodeTab[lpc] == NULL) {
             continue;
         }
 
         xml = xpathObj->nodesetval->nodeTab[lpc]->parent;
 
         for (; xml; xml = xml->parent) {
             int lpc2 = 0;
 
             for (lpc2 = 0; lpc2 < max; lpc2++) {
                 if (xpathObj->nodesetval->nodeTab[lpc2] == xml) {
                     xpathObj->nodesetval->nodeTab[lpc] = NULL;
                     dedup = TRUE;
                     break;
                 }
             }
 
             if (dedup) {
                 break;
             }
         }
     }
 }
 
 /* the caller needs to check if the result contains a xmlDocPtr or xmlNodePtr */
 xmlXPathObjectPtr
 xpath_search(const xmlNode *xml_top, const char *path)
 {
     xmlXPathObjectPtr xpathObj = NULL;
     xmlXPathContextPtr xpathCtx = NULL;
     const xmlChar *xpathExpr = (pcmkXmlStr) path;
 
     CRM_CHECK(path != NULL, return NULL);
     CRM_CHECK(xml_top != NULL, return NULL);
     CRM_CHECK(strlen(path) > 0, return NULL);
 
     xpathCtx = xmlXPathNewContext(xml_top->doc);
     pcmk__mem_assert(xpathCtx);
 
     xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
     xmlXPathFreeContext(xpathCtx);
     return xpathObj;
 }
 
 /*!
  * \brief Run a supplied function for each result of an xpath search
  *
  * \param[in,out] xml        XML to search
  * \param[in]     xpath      XPath search string
  * \param[in]     helper     Function to call for each result
  * \param[in,out] user_data  Data to pass to supplied function
  *
  * \note The helper function will be passed the XML node of the result,
  *       and the supplied user_data. This function does not otherwise
  *       use user_data.
  */
 void
 crm_foreach_xpath_result(xmlNode *xml, const char *xpath,
                          void (*helper)(xmlNode*, void*), void *user_data)
 {
     xmlXPathObjectPtr xpathObj = xpath_search(xml, xpath);
     int nresults = numXpathResults(xpathObj);
     int i;
 
     for (i = 0; i < nresults; i++) {
         xmlNode *result = getXpathResult(xpathObj, i);
 
         CRM_LOG_ASSERT(result != NULL);
         if (result) {
             (*helper)(result, user_data);
         }
     }
     freeXpathObject(xpathObj);
 }
 
 xmlNode *
 get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level)
 {
     int max;
     xmlNode *result = NULL;
     xmlXPathObjectPtr xpathObj = NULL;
     char *nodePath = NULL;
     char *matchNodePath = NULL;
 
     if (xpath == NULL) {
         return xml_obj;         /* or return NULL? */
     }
 
     xpathObj = xpath_search(xml_obj, xpath);
     nodePath = (char *)xmlGetNodePath(xml_obj);
     max = numXpathResults(xpathObj);
 
     if (max < 1) {
         if (error_level < LOG_NEVER) {
             do_crm_log(error_level, "No match for %s in %s",
                        xpath, pcmk__s(nodePath, "unknown path"));
             crm_log_xml_explicit(xml_obj, "Unexpected Input");
         }
 
     } else if (max > 1) {
         if (error_level < LOG_NEVER) {
             int lpc = 0;
 
             do_crm_log(error_level, "Too many matches for %s in %s",
                        xpath, pcmk__s(nodePath, "unknown path"));
 
             for (lpc = 0; lpc < max; lpc++) {
                 xmlNode *match = getXpathResult(xpathObj, lpc);
 
                 CRM_LOG_ASSERT(match != NULL);
                 if (match != NULL) {
                     matchNodePath = (char *) xmlGetNodePath(match);
                     do_crm_log(error_level, "%s[%d] = %s",
                                xpath, lpc,
                                pcmk__s(matchNodePath, "unrecognizable match"));
                     free(matchNodePath);
                 }
             }
             crm_log_xml_explicit(xml_obj, "Bad Input");
         }
 
     } else {
         result = getXpathResult(xpathObj, 0);
     }
 
     freeXpathObject(xpathObj);
     free(nodePath);
 
     return result;
 }
 
 /*!
  * \internal
  * \brief Get an XPath string that matches an XML element as closely as possible
  *
  * \param[in] xml  The XML element for which to build an XPath string
  *
  * \return A \p GString that matches \p xml, or \p NULL if \p xml is \p NULL.
  *
  * \note The caller is responsible for freeing the string using
  *       \p g_string_free().
  */
 GString *
 pcmk__element_xpath(const xmlNode *xml)
 {
     const xmlNode *parent = NULL;
     GString *xpath = NULL;
     const char *id = NULL;
 
     if (xml == NULL) {
         return NULL;
     }
 
     parent = xml->parent;
     xpath = pcmk__element_xpath(parent);
     if (xpath == NULL) {
         xpath = g_string_sized_new(256);
     }
 
     // Build xpath like "/" -> "/cib" -> "/cib/configuration"
     if (parent == NULL) {
         g_string_append_c(xpath, '/');
     } else if (parent->parent == NULL) {
         g_string_append(xpath, (const gchar *) xml->name);
     } else {
         pcmk__g_strcat(xpath, "/", (const char *) xml->name, NULL);
     }
 
     id = pcmk__xe_id(xml);
     if (id != NULL) {
         pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", id, "']", NULL);
     }
 
     return xpath;
 }
 
 char *
 pcmk__xpath_node_id(const char *xpath, const char *node)
 {
     char *retval = NULL;
     char *patt = NULL;
     char *start = NULL;
     char *end = NULL;
 
     if (node == NULL || xpath == NULL) {
         return retval;
     }
 
     patt = crm_strdup_printf("/%s[@" PCMK_XA_ID "=", node);
     start = strstr(xpath, patt);
 
     if (!start) {
         free(patt);
         return retval;
     }
 
     start += strlen(patt);
     start++;
 
     end = strstr(start, "\'");
     CRM_ASSERT(end);
     retval = strndup(start, end-start);
 
     free(patt);
     return retval;
 }
 
 static int
 output_attr_child(xmlNode *child, void *userdata)
 {
     pcmk__output_t *out = userdata;
 
     out->info(out, "  Value: %s \t(id=%s)",
               crm_element_value(child, PCMK_XA_VALUE),
               pcmk__s(pcmk__xe_id(child), "<none>"));
     return pcmk_rc_ok;
 }
 
 void
 pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search,
                                  const char *name)
 {
     if (out == NULL || name == NULL || search == NULL ||
         search->children == NULL) {
         return;
     }
 
     out->info(out, "Multiple attributes match " PCMK_XA_NAME "=%s", name);
     pcmk__xe_foreach_child(search, NULL, output_attr_child, out);
 }
-
-// Deprecated functions kept only for backward API compatibility
-// LCOV_EXCL_START
-
-#include <crm/common/xml_compat.h>
-
-/*!
- * \deprecated This function will be removed in a future release
- * \brief Get an XPath string that matches an XML element as closely as possible
- *
- * \param[in] xml  The XML element for which to build an XPath string
- *
- * \return A string that matches \p xml, or \p NULL if \p xml is \p NULL.
- *
- * \note The caller is responsible for freeing the string using free().
- */
-char *
-xml_get_path(const xmlNode *xml)
-{
-    char *path = NULL;
-    GString *g_path = pcmk__element_xpath(xml);
-
-    if (g_path == NULL) {
-        return NULL;
-    }
-    path = pcmk__str_copy(g_path->str);
-    g_string_free(g_path, TRUE);
-    return path;
-}
-
-xmlNode *
-get_xpath_object_relative(const char *xpath, xmlNode *xml_obj, int error_level)
-{
-    xmlNode *result = NULL;
-    char *xpath_full = NULL;
-    char *xpath_prefix = NULL;
-
-    if (xml_obj == NULL || xpath == NULL) {
-        return NULL;
-    }
-
-    xpath_prefix = (char *)xmlGetNodePath(xml_obj);
-
-    xpath_full = crm_strdup_printf("%s%s", xpath_prefix, xpath);
-
-    result = get_xpath_object(xpath_full, xml_obj, error_level);
-
-    free(xpath_prefix);
-    free(xpath_full);
-    return result;
-}
-
-// LCOV_EXCL_STOP
-// End deprecated API