root/lib/common/xml_io.c

/* [previous][next][first][last][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. decompress_file
  2. pcmk__xml_read
  3. pcmk__xml_parse
  4. dump_xml_element
  5. dump_xml_text
  6. dump_xml_cdata
  7. dump_xml_comment
  8. pcmk__xml_string
  9. write_compressed_stream
  10. write_xml_stream
  11. pcmk__xml_write_fd
  12. pcmk__xml_write_file
  13. pcmk__xml2fd
  14. save_xml_to_file

   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 <stdio.h>
  13 #include <stdlib.h>
  14 #include <string.h>
  15 #include <sys/types.h>
  16 
  17 #include <bzlib.h>
  18 #include <libxml/parser.h>
  19 #include <libxml/tree.h>
  20 #include <libxml/xmlIO.h>               // xmlOutputBuffer*
  21 #include <libxml/xmlstring.h>           // xmlChar
  22 
  23 #include <crm/crm.h>
  24 #include <crm/common/xml.h>
  25 #include <crm/common/xml_io.h>
  26 #include "crmcommon_private.h"
  27 
  28 /*!
  29  * \internal
  30  * \brief Decompress a <tt>bzip2</tt>-compressed file into a string buffer
  31  *
  32  * \param[in] filename  Name of file to decompress
  33  *
  34  * \return Newly allocated string with the decompressed contents of \p filename,
  35  *         or \c NULL on error.
  36  *
  37  * \note The caller is responsible for freeing the return value using \c free().
  38  */
  39 static char *
  40 decompress_file(const char *filename)
     /* [previous][next][first][last][top][bottom][index][help] */
  41 {
  42     char *buffer = NULL;
  43     int rc = pcmk_rc_ok;
  44     size_t length = 0;
  45     BZFILE *bz_file = NULL;
  46     FILE *input = fopen(filename, "r");
  47 
  48     if (input == NULL) {
  49         crm_perror(LOG_ERR, "Could not open %s for reading", filename);
  50         return NULL;
  51     }
  52 
  53     bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0);
  54     rc = pcmk__bzlib2rc(rc);
  55     if (rc != pcmk_rc_ok) {
  56         crm_err("Could not prepare to read compressed %s: %s "
  57                 QB_XS " rc=%d", filename, pcmk_rc_str(rc), rc);
  58         goto done;
  59     }
  60 
  61     do {
  62         int read_len = 0;
  63 
  64         buffer = pcmk__realloc(buffer, length + PCMK__BUFFER_SIZE + 1);
  65         read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE);
  66 
  67         if ((rc == BZ_OK) || (rc == BZ_STREAM_END)) {
  68             crm_trace("Read %ld bytes from file: %d", (long) read_len, rc);
  69             length += read_len;
  70         }
  71     } while (rc == BZ_OK);
  72 
  73     rc = pcmk__bzlib2rc(rc);
  74     if (rc != pcmk_rc_ok) {
  75         rc = pcmk__bzlib2rc(rc);
  76         crm_err("Could not read compressed %s: %s " QB_XS " rc=%d",
  77                 filename, pcmk_rc_str(rc), rc);
  78         free(buffer);
  79         buffer = NULL;
  80     } else {
  81         buffer[length] = '\0';
  82     }
  83 
  84 done:
  85     BZ2_bzReadClose(&rc, bz_file);
  86     fclose(input);
  87     return buffer;
  88 }
  89 
  90 /*!
  91  * \internal
  92  * \brief Parse XML from a file
  93  *
  94  * \param[in] filename  Name of file containing XML (\c NULL or \c "-" for
  95  *                      \c stdin); if \p filename ends in \c ".bz2", the file
  96  *                      will be decompressed using \c bzip2
  97  *
  98  * \return XML tree parsed from the given file on success, otherwise \c NULL
  99  */
 100 xmlNode *
 101 pcmk__xml_read(const char *filename)
     /* [previous][next][first][last][top][bottom][index][help] */
 102 {
 103     bool use_stdin = pcmk__str_eq(filename, "-", pcmk__str_null_matches);
 104     xmlNode *xml = NULL;
 105     xmlDoc *output = NULL;
 106     xmlParserCtxt *ctxt = NULL;
 107     const xmlError *last_error = NULL;
 108 
 109     // Create a parser context
 110     ctxt = xmlNewParserCtxt();
 111     CRM_CHECK(ctxt != NULL, return NULL);
 112 
 113     xmlCtxtResetLastError(ctxt);
 114     xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err);
 115 
 116     if (use_stdin) {
 117         output = xmlCtxtReadFd(ctxt, STDIN_FILENO, NULL, NULL,
 118                                XML_PARSE_NOBLANKS);
 119 
 120     } else if (pcmk__ends_with_ext(filename, ".bz2")) {
 121         char *input = decompress_file(filename);
 122 
 123         if (input != NULL) {
 124             output = xmlCtxtReadDoc(ctxt, (const xmlChar *) input, NULL, NULL,
 125                                     XML_PARSE_NOBLANKS);
 126             free(input);
 127         }
 128 
 129     } else {
 130         output = xmlCtxtReadFile(ctxt, filename, NULL, XML_PARSE_NOBLANKS);
 131     }
 132 
 133     if (output != NULL) {
 134         pcmk__xml_new_private_data((xmlNode *) output);
 135         xml = xmlDocGetRootElement(output);
 136         if (xml != NULL) {
 137             /* @TODO Should we really be stripping out text? This seems like an
 138              * overly broad way to get rid of whitespace, if that's the goal.
 139              * Text nodes may be invalid in most or all Pacemaker inputs, but
 140              * stripping them in a generic "parse XML from file" function may
 141              * not be the best way to ignore them.
 142              */
 143             pcmk__strip_xml_text(xml);
 144         }
 145     }
 146 
 147     last_error = xmlCtxtGetLastError(ctxt);
 148     if ((last_error != NULL) && (xml != NULL)) {
 149         crm_log_xml_debug(xml, "partial");
 150         pcmk__xml_free(xml);
 151         xml = NULL;
 152     }
 153 
 154     xmlFreeParserCtxt(ctxt);
 155     return xml;
 156 }
 157 
 158 /*!
 159  * \internal
 160  * \brief Parse XML from a string
 161  *
 162  * \param[in] input  String to parse
 163  *
 164  * \return XML tree parsed from the given string on success, otherwise \c NULL
 165  */
 166 xmlNode *
 167 pcmk__xml_parse(const char *input)
     /* [previous][next][first][last][top][bottom][index][help] */
 168 {
 169     xmlNode *xml = NULL;
 170     xmlDoc *output = NULL;
 171     xmlParserCtxt *ctxt = NULL;
 172     const xmlError *last_error = NULL;
 173 
 174     if (input == NULL) {
 175         return NULL;
 176     }
 177 
 178     ctxt = xmlNewParserCtxt();
 179     if (ctxt == NULL) {
 180         return NULL;
 181     }
 182 
 183     xmlCtxtResetLastError(ctxt);
 184     xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err);
 185 
 186     output = xmlCtxtReadDoc(ctxt, (const xmlChar *) input, NULL, NULL,
 187                             XML_PARSE_NOBLANKS);
 188     if (output != NULL) {
 189         pcmk__xml_new_private_data((xmlNode *) output);
 190         xml = xmlDocGetRootElement(output);
 191     }
 192 
 193     last_error = xmlCtxtGetLastError(ctxt);
 194     if ((last_error != NULL) && (xml != NULL)) {
 195         crm_log_xml_debug(xml, "partial");
 196         pcmk__xml_free(xml);
 197         xml = NULL;
 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,
     /* [previous][next][first][last][top][bottom][index][help] */
 215                  int depth)
 216 {
 217     bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
 218     bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered);
 219     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,
     /* [previous][next][first][last][top][bottom][index][help] */
 275               int depth)
 276 {
 277     bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
 278     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,
     /* [previous][next][first][last][top][bottom][index][help] */
 310                int depth)
 311 {
 312     bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
 313     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,
     /* [previous][next][first][last][top][bottom][index][help] */
 338                  int depth)
 339 {
 340     bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
 341     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,
     /* [previous][next][first][last][top][bottom][index][help] */
 371                  int depth)
 372 {
 373     if (data == NULL) {
 374         crm_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             crm_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,
     /* [previous][next][first][last][top][bottom][index][help] */
 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         crm_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         crm_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         crm_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     crm_trace("Compressed XML for %s from %u bytes to %u",
 453               filename, bytes_in, *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,
     /* [previous][next][first][last][top][bottom][index][help] */
 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               crm_log_xml_info(xml, "dump-failed");
 485               rc = pcmk_rc_error;
 486               goto done);
 487 
 488     crm_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         crm_perror(LOG_ERR, "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         crm_perror(LOG_ERR, "flushing %s", filename);
 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         crm_perror(LOG_ERR, "synchronizing %s", filename);
 515     }
 516 
 517     fclose(stream);
 518     crm_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)
     /* [previous][next][first][last][top][bottom][index][help] */
 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)
     /* [previous][next][first][last][top][bottom][index][help] */
 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)
     /* [previous][next][first][last][top][bottom][index][help] */
 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 void
 604 save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename)
     /* [previous][next][first][last][top][bottom][index][help] */
 605 {
 606     char *f = NULL;
 607 
 608     if (filename == NULL) {
 609         char *uuid = crm_generate_uuid();
 610 
 611         f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid);
 612         filename = f;
 613         free(uuid);
 614     }
 615 
 616     crm_info("Saving %s to %s", desc, filename);
 617     pcmk__xml_write_file(xml, filename, false);
 618     free(f);
 619 }

/* [previous][next][first][last][top][bottom][index][help] */