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