1    	/*
2    	 * Copyright 2004-2026 the Pacemaker project contributors
3    	 *
4    	 * The version control history for this file may have further details.
5    	 *
6    	 * This source code is licensed under the GNU Lesser General Public License
7    	 * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
8    	 */
9    	
10   	#include <crm_internal.h>
11   	
12   	#include <stdbool.h>
13   	#include <stdio.h>
14   	#include <stdlib.h>
15   	#include <string.h>
16   	#include <sys/types.h>
17   	
18   	#include <bzlib.h>
19   	#include <libxml/parser.h>
20   	#include <libxml/tree.h>
21   	#include <libxml/xmlIO.h>               // xmlOutputBuffer*
22   	#include <libxml/xmlstring.h>           // xmlChar
23   	
24   	#include <crm/crm.h>
25   	#include <crm/common/xml.h>
26   	#include <crm/common/xml_io.h>
27   	#include "crmcommon_private.h"
28   	
29   	/*!
30   	 * \internal
31   	 * \brief Decompress a <tt>bzip2</tt>-compressed file into a string buffer
32   	 *
33   	 * \param[in] filename  Name of file to decompress
34   	 *
35   	 * \return Newly allocated string with the decompressed contents of \p filename,
36   	 *         or \c NULL on error.
37   	 *
38   	 * \note The caller is responsible for freeing the return value using \c free().
39   	 */
40   	static char *
41   	decompress_file(const char *filename)
42   	{
43   	    char *buffer = NULL;
44   	    int rc = pcmk_rc_ok;
45   	    size_t length = 0;
46   	    BZFILE *bz_file = NULL;
47   	    FILE *input = fopen(filename, "r");
48   	
49   	    if (input == NULL) {
50   	        pcmk__err("Could not open %s for reading: %s", filename,
51   	                  strerror(errno));
52   	        return NULL;
53   	    }
54   	
55   	    bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0);
56   	    rc = pcmk__bzlib2rc(rc);
57   	    if (rc != pcmk_rc_ok) {
58   	        pcmk__err("Could not prepare to read compressed %s: %s " QB_XS " rc=%d",
59   	                  filename, pcmk_rc_str(rc), rc);
60   	        goto done;
61   	    }
62   	
63   	    do {
64   	        int read_len = 0;
65   	
66   	        buffer = pcmk__realloc(buffer, length + PCMK__BUFFER_SIZE + 1);
67   	        read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE);
68   	
69   	        if ((rc == BZ_OK) || (rc == BZ_STREAM_END)) {
70   	            pcmk__trace("Read %ld bytes from file: %d", (long) read_len, rc);
71   	            length += read_len;
72   	        }
73   	    } while (rc == BZ_OK);
74   	
75   	    rc = pcmk__bzlib2rc(rc);
76   	    if (rc != pcmk_rc_ok) {
77   	        rc = pcmk__bzlib2rc(rc);
78   	        pcmk__err("Could not read compressed %s: %s " QB_XS " rc=%d", filename,
79   	                  pcmk_rc_str(rc), rc);
80   	        g_clear_pointer(&buffer, free);
81   	
82   	    } else {
83   	        buffer[length] = '\0';
84   	    }
85   	
86   	done:
87   	    BZ2_bzReadClose(&rc, bz_file);
88   	    fclose(input);
89   	    return buffer;
90   	}
91   	
92   	/*!
93   	 * \internal
94   	 * \brief Parse XML from a file
95   	 *
96   	 * \param[in] filename  Name of file containing XML (\c NULL or \c "-" for
97   	 *                      \c stdin); if \p filename ends in \c ".bz2", the file
98   	 *                      will be decompressed using \c bzip2
99   	 *
100  	 * \return XML tree parsed from the given file on success, otherwise \c NULL
101  	 */
102  	xmlNode *
103  	pcmk__xml_read(const char *filename)
104  	{
105  	    bool use_stdin = pcmk__str_eq(filename, "-", pcmk__str_null_matches);
106  	    xmlNode *xml = NULL;
107  	    xmlDoc *output = NULL;
108  	    xmlParserCtxt *ctxt = NULL;
109  	    const xmlError *last_error = NULL;
110  	
111  	    // Create a parser context
112  	    ctxt = xmlNewParserCtxt();
(1) Event path: Condition "!(ctxt != NULL)", taking false branch.
113  	    CRM_CHECK(ctxt != NULL, return NULL);
114  	
115  	    xmlCtxtResetLastError(ctxt);
116  	    xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err);
117  	
(2) Event path: Condition "use_stdin", taking true branch.
118  	    if (use_stdin) {
119  	        output = xmlCtxtReadFd(ctxt, STDIN_FILENO, NULL, NULL,
120  	                               XML_PARSE_NOBLANKS);
121  	
(3) Event path: Falling through to end of if statement.
122  	    } else if (g_str_has_suffix(filename, ".bz2")) {
123  	        char *input = decompress_file(filename);
124  	
125  	        if (input != NULL) {
126  	            output = xmlCtxtReadDoc(ctxt, (const xmlChar *) input, NULL, NULL,
127  	                                    XML_PARSE_NOBLANKS);
128  	            free(input);
129  	        }
130  	
131  	    } else {
132  	        output = xmlCtxtReadFile(ctxt, filename, NULL, XML_PARSE_NOBLANKS);
133  	    }
134  	
(4) Event path: Condition "output != NULL", taking true branch.
135  	    if (output != NULL) {
136  	        pcmk__xml_new_private_data((xmlNode *) output);
137  	        xml = xmlDocGetRootElement(output);
(5) Event path: Condition "xml != NULL", taking true branch.
138  	        if (xml != NULL) {
139  	            /* @TODO Should we really be stripping out text? This seems like an
140  	             * overly broad way to get rid of whitespace, if that's the goal.
141  	             * Text nodes may be invalid in most or all Pacemaker inputs, but
142  	             * stripping them in a generic "parse XML from file" function may
143  	             * not be the best way to ignore them.
144  	             */
145  	            pcmk__strip_xml_text(xml);
146  	        }
147  	    }
148  	
149  	    last_error = xmlCtxtGetLastError(ctxt);
(6) Event path: Condition "last_error != NULL", taking true branch.
(7) Event path: Condition "xml != NULL", taking true branch.
150  	    if ((last_error != NULL) && (xml != NULL)) {
(8) Event path: Switch case default.
(9) Event path: Condition "xml_cs == NULL", taking true branch.
(10) Event path: Condition "crm_is_callsite_active(xml_cs, _level, 0)", taking false branch.
(11) Event path: Breaking from switch.
151  	        pcmk__log_xml_debug(xml, "partial");
CID (unavailable; MK=86ea132a9611ed4d26ffa96ceb116cd5) (#1 of 1): Inconsistent C union access (INCONSISTENT_UNION_ACCESS):
(12) Event assign_union_field: The union field "in" of "_pp" is written.
(13) Event inconsistent_union_field_access: In "_pp.out", the union field used: "out" is inconsistent with the field most recently stored: "in".
152  	        g_clear_pointer(&xml, pcmk__xml_free);
153  	    }
154  	
155  	    xmlFreeParserCtxt(ctxt);
156  	    return xml;
157  	}
158  	
159  	/*!
160  	 * \internal
161  	 * \brief Parse XML from a string
162  	 *
163  	 * \param[in] input  String to parse
164  	 *
165  	 * \return XML tree parsed from the given string on success, otherwise \c NULL
166  	 */
167  	xmlNode *
168  	pcmk__xml_parse(const char *input)
169  	{
170  	    xmlNode *xml = NULL;
171  	    xmlDoc *output = NULL;
172  	    xmlParserCtxt *ctxt = NULL;
173  	    const xmlError *last_error = NULL;
174  	
175  	    if (input == NULL) {
176  	        return NULL;
177  	    }
178  	
179  	    ctxt = xmlNewParserCtxt();
180  	    if (ctxt == NULL) {
181  	        return NULL;
182  	    }
183  	
184  	    xmlCtxtResetLastError(ctxt);
185  	    xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err);
186  	
187  	    output = xmlCtxtReadDoc(ctxt, (const xmlChar *) input, NULL, NULL,
188  	                            XML_PARSE_NOBLANKS);
189  	    if (output != NULL) {
190  	        pcmk__xml_new_private_data((xmlNode *) output);
191  	        xml = xmlDocGetRootElement(output);
192  	    }
193  	
194  	    last_error = xmlCtxtGetLastError(ctxt);
195  	    if ((last_error != NULL) && (xml != NULL)) {
196  	        pcmk__log_xml_debug(xml, "partial");
197  	        g_clear_pointer(&xml, pcmk__xml_free);
198  	    }
199  	
200  	    xmlFreeParserCtxt(ctxt);
201  	    return xml;
202  	}
203  	
204  	/*!
205  	 * \internal
206  	 * \brief Append a string representation of an XML element to a buffer
207  	 *
208  	 * \param[in]     data     XML whose representation to append
209  	 * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
210  	 * \param[in,out] buffer   Where to append the content (must not be \p NULL)
211  	 * \param[in]     depth    Current indentation level
212  	 */
213  	static void
214  	dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer,
215  	                 int depth)
216  	{
217  	    const bool pretty = pcmk__is_set(options, pcmk__xml_fmt_pretty);
218  	    const bool filtered = pcmk__is_set(options, pcmk__xml_fmt_filtered);
219  	    const int spaces = pretty? (2 * depth) : 0;
220  	
221  	    for (int lpc = 0; lpc < spaces; lpc++) {
222  	        g_string_append_c(buffer, ' ');
223  	    }
224  	
225  	    pcmk__g_strcat(buffer, "<", data->name, NULL);
226  	
227  	    for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL;
228  	         attr = attr->next) {
229  	
230  	        if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) {
231  	            pcmk__dump_xml_attr(attr, buffer);
232  	        }
233  	    }
234  	
235  	    if (data->children == NULL) {
236  	        g_string_append(buffer, "/>");
237  	
238  	    } else {
239  	        g_string_append_c(buffer, '>');
240  	    }
241  	
242  	    if (pretty) {
243  	        g_string_append_c(buffer, '\n');
244  	    }
245  	
246  	    if (data->children) {
247  	        for (const xmlNode *child = data->children; child != NULL;
248  	             child = child->next) {
249  	            pcmk__xml_string(child, options, buffer, depth + 1);
250  	        }
251  	
252  	        for (int lpc = 0; lpc < spaces; lpc++) {
253  	            g_string_append_c(buffer, ' ');
254  	        }
255  	
256  	        pcmk__g_strcat(buffer, "</", data->name, ">", NULL);
257  	
258  	        if (pretty) {
259  	            g_string_append_c(buffer, '\n');
260  	        }
261  	    }
262  	}
263  	
264  	/*!
265  	 * \internal
266  	 * \brief Append XML text content to a buffer
267  	 *
268  	 * \param[in]     data     XML whose content to append
269  	 * \param[in]     options  Group of <tt>enum pcmk__xml_fmt_options</tt>
270  	 * \param[in,out] buffer   Where to append the content (must not be \p NULL)
271  	 * \param[in]     depth    Current indentation level
272  	 */
273  	static void
274  	dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer,
275  	              int depth)
276  	{
277  	    const bool pretty = pcmk__is_set(options, pcmk__xml_fmt_pretty);
278  	    const int spaces = pretty? (2 * depth) : 0;
279  	    const char *content = (const char *) data->content;
280  	    gchar *content_esc = NULL;
281  	
282  	    if (pcmk__xml_needs_escape(content, pcmk__xml_escape_text)) {
283  	        content_esc = pcmk__xml_escape(content, pcmk__xml_escape_text);
284  	        content = content_esc;
285  	    }
286  	
287  	    for (int lpc = 0; lpc < spaces; lpc++) {
288  	        g_string_append_c(buffer, ' ');
289  	    }
290  	
291  	    g_string_append(buffer, content);
292  	
293  	    if (pretty) {
294  	        g_string_append_c(buffer, '\n');
295  	    }
296  	    g_free(content_esc);
297  	}
298  	
299  	/*!
300  	 * \internal
301  	 * \brief Append XML CDATA content to a buffer
302  	 *
303  	 * \param[in]     data     XML whose content to append
304  	 * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
305  	 * \param[in,out] buffer   Where to append the content (must not be \p NULL)
306  	 * \param[in]     depth    Current indentation level
307  	 */
308  	static void
309  	dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer,
310  	               int depth)
311  	{
312  	    const bool pretty = pcmk__is_set(options, pcmk__xml_fmt_pretty);
313  	    const int spaces = pretty? (2 * depth) : 0;
314  	
315  	    for (int lpc = 0; lpc < spaces; lpc++) {
316  	        g_string_append_c(buffer, ' ');
317  	    }
318  	
319  	    pcmk__g_strcat(buffer, "<![CDATA[", (const char *) data->content, "]]>",
320  	                   NULL);
321  	
322  	    if (pretty) {
323  	        g_string_append_c(buffer, '\n');
324  	    }
325  	}
326  	
327  	/*!
328  	 * \internal
329  	 * \brief Append an XML comment to a buffer
330  	 *
331  	 * \param[in]     data     XML whose content to append
332  	 * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
333  	 * \param[in,out] buffer   Where to append the content (must not be \p NULL)
334  	 * \param[in]     depth    Current indentation level
335  	 */
336  	static void
337  	dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer,
338  	                 int depth)
339  	{
340  	    const bool pretty = pcmk__is_set(options, pcmk__xml_fmt_pretty);
341  	    const int spaces = pretty? (2 * depth) : 0;
342  	
343  	    for (int lpc = 0; lpc < spaces; lpc++) {
344  	        g_string_append_c(buffer, ' ');
345  	    }
346  	
347  	    pcmk__g_strcat(buffer, "<!--", (const char *) data->content, "-->", NULL);
348  	
349  	    if (pretty) {
350  	        g_string_append_c(buffer, '\n');
351  	    }
352  	}
353  	
354  	/*!
355  	 * \internal
356  	 * \brief Create a string representation of an XML object
357  	 *
358  	 * libxml2's \c xmlNodeDumpOutput() doesn't allow filtering, doesn't escape
359  	 * special characters thoroughly, and doesn't allow a const argument.
360  	 *
361  	 * \param[in]     data     XML to convert
362  	 * \param[in]     options  Group of \p pcmk__xml_fmt_options flags
363  	 * \param[in,out] buffer   Where to store the text (must not be \p NULL)
364  	 * \param[in]     depth    Current indentation level
365  	 *
366  	 * \todo Create a wrapper that doesn't require \p depth. Only used with
367  	 *       recursive calls currently.
368  	 */
369  	void
370  	pcmk__xml_string(const xmlNode *data, uint32_t options, GString *buffer,
371  	                 int depth)
372  	{
373  	    if (data == NULL) {
374  	        pcmk__trace("Nothing to dump");
375  	        return;
376  	    }
377  	
378  	    pcmk__assert(buffer != NULL);
379  	    CRM_CHECK(depth >= 0, depth = 0);
380  	
381  	    switch(data->type) {
382  	        case XML_ELEMENT_NODE:
383  	            /* Handle below */
384  	            dump_xml_element(data, options, buffer, depth);
385  	            break;
386  	        case XML_TEXT_NODE:
387  	            if (pcmk__is_set(options, pcmk__xml_fmt_text)) {
388  	                dump_xml_text(data, options, buffer, depth);
389  	            }
390  	            break;
391  	        case XML_COMMENT_NODE:
392  	            dump_xml_comment(data, options, buffer, depth);
393  	            break;
394  	        case XML_CDATA_SECTION_NODE:
395  	            dump_xml_cdata(data, options, buffer, depth);
396  	            break;
397  	        default:
398  	            pcmk__warn("Cannot convert XML %s node to text " QB_XS " type=%d",
399  	                       pcmk__xml_element_type_text(data->type), data->type);
400  	            break;
401  	    }
402  	}
403  	
404  	/*!
405  	 * \internal
406  	 * \brief Write a string to a file stream, compressed using \c bzip2
407  	 *
408  	 * \param[in]     text       String to write
409  	 * \param[in]     filename   Name of file being written (for logging only)
410  	 * \param[in,out] stream     Open file stream to write to
411  	 * \param[out]    bytes_out  Number of bytes written (valid only on success)
412  	 *
413  	 * \return Standard Pacemaker return code
414  	 */
415  	static int
416  	write_compressed_stream(char *text, const char *filename, FILE *stream,
417  	                        unsigned int *bytes_out)
418  	{
419  	    unsigned int bytes_in = 0;
420  	    int rc = pcmk_rc_ok;
421  	
422  	    // (5, 0, 0): (intermediate block size, silent, default workFactor)
423  	    BZFILE *bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 0);
424  	
425  	    rc = pcmk__bzlib2rc(rc);
426  	    if (rc != pcmk_rc_ok) {
427  	        pcmk__warn("Not compressing %s: could not prepare file stream: %s "
428  	                   QB_XS " rc=%d",
429  	                   filename, pcmk_rc_str(rc), rc);
430  	        goto done;
431  	    }
432  	
433  	    BZ2_bzWrite(&rc, bz_file, text, strlen(text));
434  	    rc = pcmk__bzlib2rc(rc);
435  	    if (rc != pcmk_rc_ok) {
436  	        pcmk__warn("Not compressing %s: could not compress data: %s "
437  	                   QB_XS " rc=%d errno=%d",
438  	                   filename, pcmk_rc_str(rc), rc, errno);
439  	        goto done;
440  	    }
441  	
442  	    BZ2_bzWriteClose(&rc, bz_file, 0, &bytes_in, bytes_out);
443  	    bz_file = NULL;
444  	    rc = pcmk__bzlib2rc(rc);
445  	    if (rc != pcmk_rc_ok) {
446  	        pcmk__warn("Not compressing %s: could not write compressed data: %s "
447  	                   QB_XS " rc=%d errno=%d",
448  	                   filename, pcmk_rc_str(rc), rc, errno);
449  	        goto done;
450  	    }
451  	
452  	    pcmk__trace("Compressed XML for %s from %u bytes to %u", filename, bytes_in,
453  	                *bytes_out);
454  	
455  	done:
456  	    if (bz_file != NULL) {
457  	        BZ2_bzWriteClose(&rc, bz_file, 0, NULL, NULL);
458  	    }
459  	    return rc;
460  	}
461  	
462  	/*!
463  	 * \internal
464  	 * \brief Write XML to a file stream
465  	 *
466  	 * \param[in]     xml       XML to write
467  	 * \param[in]     filename  Name of file being written (for logging only)
468  	 * \param[in,out] stream    Open file stream corresponding to filename (closed
469  	 *                          when this function returns)
470  	 * \param[in]     compress  Whether to compress XML before writing
471  	 *
472  	 * \return Standard Pacemaker return code
473  	 */
474  	static int
475  	write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream,
476  	                 bool compress)
477  	{
478  	    GString *buffer = g_string_sized_new(1024);
479  	    unsigned int bytes_out = 0;
480  	    int rc = pcmk_rc_ok;
481  	
482  	    pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0);
483  	    CRM_CHECK(!pcmk__str_empty(buffer->str),
484  	              pcmk__log_xml_info(xml, "dump-failed");
485  	              rc = pcmk_rc_error;
486  	              goto done);
487  	
488  	    pcmk__log_xml_trace(xml, "writing");
489  	
490  	    if (compress
491  	        && (write_compressed_stream(buffer->str, filename, stream,
492  	                                    &bytes_out) == pcmk_rc_ok)) {
493  	        goto done;
494  	    }
495  	
496  	    rc = fprintf(stream, "%s", buffer->str);
497  	    if (rc < 0) {
498  	        rc = EIO;
499  	        pcmk__err("Error writing %s", filename);
500  	        goto done;
501  	    }
502  	    bytes_out = (unsigned int) rc;
503  	    rc = pcmk_rc_ok;
504  	
505  	done:
506  	    if (fflush(stream) != 0) {
507  	        rc = errno;
508  	        pcmk__err("Error flushing %s: %s", filename, strerror(errno));
509  	    }
510  	
511  	    // Don't report error if the file does not support synchronization
512  	    if ((fsync(fileno(stream)) < 0) && (errno != EROFS) && (errno != EINVAL)) {
513  	        rc = errno;
514  	        pcmk__err("Error synchronizing %s: %s", filename, strerror(errno));
515  	    }
516  	
517  	    fclose(stream);
518  	    pcmk__trace("Saved %u bytes to %s as XML", bytes_out, filename);
519  	
520  	    g_string_free(buffer, TRUE);
521  	    return rc;
522  	}
523  	
524  	/*!
525  	 * \internal
526  	 * \brief Write XML to a file descriptor
527  	 *
528  	 * \param[in]  xml       XML to write
529  	 * \param[in]  filename  Name of file being written (for logging only)
530  	 * \param[in]  fd        Open file descriptor corresponding to \p filename
531  	 *
532  	 * \return Standard Pacemaker return code
533  	 */
534  	int
535  	pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd)
536  	{
537  	    FILE *stream = NULL;
538  	
539  	    CRM_CHECK((xml != NULL) && (fd > 0), return EINVAL);
540  	    stream = fdopen(fd, "w");
541  	    if (stream == NULL) {
542  	        return errno;
543  	    }
544  	
545  	    return write_xml_stream(xml, pcmk__s(filename, "unnamed file"), stream,
546  	                            false);
547  	}
548  	
549  	/*!
550  	 * \internal
551  	 * \brief Write XML to a file
552  	 *
553  	 * \param[in]  xml       XML to write
554  	 * \param[in]  filename  Name of file to write
555  	 * \param[in]  compress  If \c true, compress XML before writing
556  	 *
557  	 * \return Standard Pacemaker return code
558  	 */
559  	int
560  	pcmk__xml_write_file(const xmlNode *xml, const char *filename, bool compress)
561  	{
562  	    FILE *stream = NULL;
563  	
564  	    CRM_CHECK((xml != NULL) && (filename != NULL), return EINVAL);
565  	    stream = fopen(filename, "w");
566  	    if (stream == NULL) {
567  	        return errno;
568  	    }
569  	
570  	    return write_xml_stream(xml, filename, stream, compress);
571  	}
572  	
573  	/*!
574  	 * \internal
575  	 * \brief Serialize XML (using libxml) into provided descriptor
576  	 *
577  	 * \param[in] fd  File descriptor to (piece-wise) write to
578  	 * \param[in] cur XML subtree to proceed
579  	 *
580  	 * \return a standard Pacemaker return code
581  	 */
582  	int
583  	pcmk__xml2fd(int fd, xmlNode *cur)
584  	{
585  	    bool success;
586  	
587  	    xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL);
588  	    pcmk__mem_assert(fd_out);
589  	    xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL);
590  	
591  	    success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1;
592  	
593  	    success = xmlOutputBufferClose(fd_out) != -1 && success;
594  	
595  	    if (!success) {
596  	        return EIO;
597  	    }
598  	
599  	    fsync(fd);
600  	    return pcmk_rc_ok;
601  	}
602  	
603  	/*!
604  	 * \internal
605  	 * \brief Write XML to a file in a temporary directory
606  	 *
607  	 * \param[in] xml       XML to write
608  	 * \param[in] desc      Description of \p xml
609  	 * \param[in] filename  Base name of file to write (\c NULL to create a name
610  	 *                      based on a generated UUID)
611  	 */
612  	void
613  	pcmk__xml_write_temp_file(const xmlNode *xml, const char *desc,
614  	                          const char *filename)
615  	{
616  	    char *path = NULL;
617  	    char *uuid = NULL;
618  	
619  	    CRM_CHECK((xml != NULL) && (desc != NULL), return);
620  	
621  	    if (filename == NULL) {
622  	        uuid = pcmk__generate_uuid();
623  	        filename = uuid;
624  	    }
625  	    path = pcmk__assert_asprintf("%s/%s", pcmk__get_tmpdir(), filename);
626  	
627  	    pcmk__info("Saving %s to %s", desc, path);
628  	    pcmk__xml_write_file(xml, filename, false);
629  	
630  	    free(path);
631  	    free(uuid);
632  	}
633  	
634  	// Deprecated functions kept only for backward API compatibility
635  	// LCOV_EXCL_START
636  	
637  	#include <crm/common/xml_io_compat.h>
638  	
639  	void
640  	save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename)
641  	{
642  	    char *f = NULL;
643  	
644  	    if (filename == NULL) {
645  	        char *uuid = pcmk__generate_uuid();
646  	
647  	        f = pcmk__assert_asprintf("%s/%s", pcmk__get_tmpdir(), uuid);
648  	        filename = f;
649  	        free(uuid);
650  	    }
651  	
652  	    pcmk__info("Saving %s to %s", desc, filename);
653  	    pcmk__xml_write_file(xml, filename, false);
654  	    free(f);
655  	}
656  	
657  	// LCOV_EXCL_STOP
658  	// End deprecated API
659