diff --git a/include/crm/common/xml_io_compat.h b/include/crm/common/xml_io_compat.h
index 74e5f1da81..092a273803 100644
--- a/include/crm/common/xml_io_compat.h
+++ b/include/crm/common/xml_io_compat.h
@@ -1,58 +1,55 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_XML_IO_COMPAT__H
 #define PCMK__CRM_COMMON_XML_IO_COMPAT__H
 
 #include <glib.h>               // gboolean
 #include <libxml/tree.h>        // xmlNode
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Deprecated Pacemaker XML I/O API
  * \ingroup core
  * \deprecated Do not include this header directly. The XML APIs in this
  *             header, and the header itself, will be removed in a future
  *             release.
  */
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *filename2xml(const char *filename);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *stdin2xml(void);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *string2xml(const char *input);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 int write_xml_fd(const xmlNode *xml, const char *filename, int fd,
                  gboolean compress);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 int write_xml_file(const xmlNode *xml, const char *filename, gboolean compress);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 char *dump_xml_formatted(const xmlNode *xml);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 char *dump_xml_formatted_with_text(const xmlNode *xml);
 
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-char *dump_xml_unformatted(const xmlNode *xml);
-
 #ifdef __cplusplus
 }
 #endif
 
 #endif  // PCMK__CRM_COMMON_XML_IO_COMPAT__H
diff --git a/lib/common/xml_io.c b/lib/common/xml_io.c
index f88e0b523e..d90bf45a13 100644
--- a/lib/common/xml_io.c
+++ b/lib/common/xml_io.c
@@ -1,840 +1,827 @@
 /*
  * 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,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