Coverage Report

Created: 2025-12-31 06:13

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