summaryrefslogtreecommitdiffstats
path: root/lib/common/strings.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/common/strings.c')
-rw-r--r--lib/common/strings.c1363
1 files changed, 1363 insertions, 0 deletions
diff --git a/lib/common/strings.c b/lib/common/strings.c
new file mode 100644
index 0000000..b245102
--- /dev/null
+++ b/lib/common/strings.c
@@ -0,0 +1,1363 @@
+/*
+ * Copyright 2004-2023 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/common/results.h"
+#include <crm_internal.h>
+
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#include <regex.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <float.h> // DBL_MIN
+#include <limits.h>
+#include <bzlib.h>
+#include <sys/types.h>
+
+/*!
+ * \internal
+ * \brief Scan a long long integer from a string
+ *
+ * \param[in] text String to scan
+ * \param[out] result If not NULL, where to store scanned value
+ * \param[in] default_value Value to use if text is NULL or invalid
+ * \param[out] end_text If not NULL, where to store pointer to first
+ * non-integer character
+ *
+ * \return Standard Pacemaker return code (\c pcmk_rc_ok on success,
+ * \c EINVAL on failed string conversion due to invalid input,
+ * or \c EOVERFLOW on arithmetic overflow)
+ * \note Sets \c errno on error
+ */
+static int
+scan_ll(const char *text, long long *result, long long default_value,
+ char **end_text)
+{
+ long long local_result = default_value;
+ char *local_end_text = NULL;
+ int rc = pcmk_rc_ok;
+
+ errno = 0;
+ if (text != NULL) {
+ local_result = strtoll(text, &local_end_text, 10);
+ if (errno == ERANGE) {
+ rc = EOVERFLOW;
+ crm_warn("Integer parsed from '%s' was clipped to %lld",
+ text, local_result);
+
+ } else if (errno != 0) {
+ rc = errno;
+ local_result = default_value;
+ crm_warn("Could not parse integer from '%s' (using %lld instead): "
+ "%s", text, default_value, pcmk_rc_str(rc));
+
+ } else if (local_end_text == text) {
+ rc = EINVAL;
+ local_result = default_value;
+ crm_warn("Could not parse integer from '%s' (using %lld instead): "
+ "No digits found", text, default_value);
+ }
+
+ if ((end_text == NULL) && !pcmk__str_empty(local_end_text)) {
+ crm_warn("Characters left over after parsing '%s': '%s'",
+ text, local_end_text);
+ }
+ errno = rc;
+ }
+ if (end_text != NULL) {
+ *end_text = local_end_text;
+ }
+ if (result != NULL) {
+ *result = local_result;
+ }
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Scan a long long integer value from a string
+ *
+ * \param[in] text The string to scan (may be NULL)
+ * \param[out] result Where to store result (or NULL to ignore)
+ * \param[in] default_value Value to use if text is NULL or invalid
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__scan_ll(const char *text, long long *result, long long default_value)
+{
+ long long local_result = default_value;
+ int rc = pcmk_rc_ok;
+
+ if (text != NULL) {
+ rc = scan_ll(text, &local_result, default_value, NULL);
+ if (rc != pcmk_rc_ok) {
+ local_result = default_value;
+ }
+ }
+ if (result != NULL) {
+ *result = local_result;
+ }
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Scan an integer value from a string, constrained to a minimum
+ *
+ * \param[in] text The string to scan (may be NULL)
+ * \param[out] result Where to store result (or NULL to ignore)
+ * \param[in] minimum Value to use as default and minimum
+ *
+ * \return Standard Pacemaker return code
+ * \note If the value is larger than the maximum integer, EOVERFLOW will be
+ * returned and \p result will be set to the maximum integer.
+ */
+int
+pcmk__scan_min_int(const char *text, int *result, int minimum)
+{
+ int rc;
+ long long result_ll;
+
+ rc = pcmk__scan_ll(text, &result_ll, (long long) minimum);
+
+ if (result_ll < (long long) minimum) {
+ crm_warn("Clipped '%s' to minimum acceptable value %d", text, minimum);
+ result_ll = (long long) minimum;
+
+ } else if (result_ll > INT_MAX) {
+ crm_warn("Clipped '%s' to maximum integer %d", text, INT_MAX);
+ result_ll = (long long) INT_MAX;
+ rc = EOVERFLOW;
+ }
+
+ if (result != NULL) {
+ *result = (int) result_ll;
+ }
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Scan a TCP port number from a string
+ *
+ * \param[in] text The string to scan
+ * \param[out] port Where to store result (or NULL to ignore)
+ *
+ * \return Standard Pacemaker return code
+ * \note \p port will be -1 if \p text is NULL or invalid
+ */
+int
+pcmk__scan_port(const char *text, int *port)
+{
+ long long port_ll;
+ int rc = pcmk__scan_ll(text, &port_ll, -1LL);
+
+ if ((text != NULL) && (rc == pcmk_rc_ok) // wasn't default or invalid
+ && ((port_ll < 0LL) || (port_ll > 65535LL))) {
+ crm_warn("Ignoring port specification '%s' "
+ "not in valid range (0-65535)", text);
+ rc = (port_ll < 0LL)? pcmk_rc_before_range : pcmk_rc_after_range;
+ port_ll = -1LL;
+ }
+ if (port != NULL) {
+ *port = (int) port_ll;
+ }
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Scan a double-precision floating-point value from a string
+ *
+ * \param[in] text The string to parse
+ * \param[out] result Parsed value on success, or
+ * \c PCMK__PARSE_DBL_DEFAULT on error
+ * \param[in] default_text Default string to parse if \p text is
+ * \c NULL
+ * \param[out] end_text If not \c NULL, where to store a pointer
+ * to the position immediately after the
+ * value
+ *
+ * \return Standard Pacemaker return code (\c pcmk_rc_ok on success,
+ * \c EINVAL on failed string conversion due to invalid input,
+ * \c EOVERFLOW on arithmetic overflow, \c pcmk_rc_underflow
+ * on arithmetic underflow, or \c errno from \c strtod() on
+ * other parse errors)
+ */
+int
+pcmk__scan_double(const char *text, double *result, const char *default_text,
+ char **end_text)
+{
+ int rc = pcmk_rc_ok;
+ char *local_end_text = NULL;
+
+ CRM_ASSERT(result != NULL);
+ *result = PCMK__PARSE_DBL_DEFAULT;
+
+ text = (text != NULL) ? text : default_text;
+
+ if (text == NULL) {
+ rc = EINVAL;
+ crm_debug("No text and no default conversion value supplied");
+
+ } else {
+ errno = 0;
+ *result = strtod(text, &local_end_text);
+
+ if (errno == ERANGE) {
+ /*
+ * Overflow: strtod() returns +/- HUGE_VAL and sets errno to
+ * ERANGE
+ *
+ * Underflow: strtod() returns "a value whose magnitude is
+ * no greater than the smallest normalized
+ * positive" double. Whether ERANGE is set is
+ * implementation-defined.
+ */
+ const char *over_under;
+
+ if (QB_ABS(*result) > DBL_MIN) {
+ rc = EOVERFLOW;
+ over_under = "over";
+ } else {
+ rc = pcmk_rc_underflow;
+ over_under = "under";
+ }
+
+ crm_debug("Floating-point value parsed from '%s' would %sflow "
+ "(using %g instead)", text, over_under, *result);
+
+ } else if (errno != 0) {
+ rc = errno;
+ // strtod() set *result = 0 on parse failure
+ *result = PCMK__PARSE_DBL_DEFAULT;
+
+ crm_debug("Could not parse floating-point value from '%s' (using "
+ "%.1f instead): %s", text, PCMK__PARSE_DBL_DEFAULT,
+ pcmk_rc_str(rc));
+
+ } else if (local_end_text == text) {
+ // errno == 0, but nothing was parsed
+ rc = EINVAL;
+ *result = PCMK__PARSE_DBL_DEFAULT;
+
+ crm_debug("Could not parse floating-point value from '%s' (using "
+ "%.1f instead): No digits found", text,
+ PCMK__PARSE_DBL_DEFAULT);
+
+ } else if (QB_ABS(*result) <= DBL_MIN) {
+ /*
+ * errno == 0 and text was parsed, but value might have
+ * underflowed.
+ *
+ * ERANGE might not be set for underflow. Check magnitude
+ * of *result, but also make sure the input number is not
+ * actually zero (0 <= DBL_MIN is not underflow).
+ *
+ * This check must come last. A parse failure in strtod()
+ * also sets *result == 0, so a parse failure would match
+ * this test condition prematurely.
+ */
+ for (const char *p = text; p != local_end_text; p++) {
+ if (strchr("0.eE", *p) == NULL) {
+ rc = pcmk_rc_underflow;
+ crm_debug("Floating-point value parsed from '%s' would "
+ "underflow (using %g instead)", text, *result);
+ break;
+ }
+ }
+
+ } else {
+ crm_trace("Floating-point value parsed successfully from "
+ "'%s': %g", text, *result);
+ }
+
+ if ((end_text == NULL) && !pcmk__str_empty(local_end_text)) {
+ crm_debug("Characters left over after parsing '%s': '%s'",
+ text, local_end_text);
+ }
+ }
+
+ if (end_text != NULL) {
+ *end_text = local_end_text;
+ }
+
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Parse a guint from a string stored in a hash table
+ *
+ * \param[in] table Hash table to search
+ * \param[in] key Hash table key to use to retrieve string
+ * \param[in] default_val What to use if key has no entry in table
+ * \param[out] result If not NULL, where to store parsed integer
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__guint_from_hash(GHashTable *table, const char *key, guint default_val,
+ guint *result)
+{
+ const char *value;
+ long long value_ll;
+ int rc = pcmk_rc_ok;
+
+ CRM_CHECK((table != NULL) && (key != NULL), return EINVAL);
+
+ if (result != NULL) {
+ *result = default_val;
+ }
+
+ value = g_hash_table_lookup(table, key);
+ if (value == NULL) {
+ return pcmk_rc_ok;
+ }
+
+ rc = pcmk__scan_ll(value, &value_ll, 0LL);
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+
+ if ((value_ll < 0) || (value_ll > G_MAXUINT)) {
+ crm_warn("Could not parse non-negative integer from %s", value);
+ return ERANGE;
+ }
+
+ if (result != NULL) {
+ *result = (guint) value_ll;
+ }
+ return pcmk_rc_ok;
+}
+
+#ifndef NUMCHARS
+# define NUMCHARS "0123456789."
+#endif
+
+#ifndef WHITESPACE
+# define WHITESPACE " \t\n\r\f"
+#endif
+
+/*!
+ * \brief Parse a time+units string and return milliseconds equivalent
+ *
+ * \param[in] input String with a number and optional unit (optionally
+ * with whitespace before and/or after the number). If
+ * missing, the unit defaults to seconds.
+ *
+ * \return Milliseconds corresponding to string expression, or
+ * PCMK__PARSE_INT_DEFAULT on error
+ */
+long long
+crm_get_msec(const char *input)
+{
+ const char *num_start = NULL;
+ const char *units;
+ long long multiplier = 1000;
+ long long divisor = 1;
+ long long msec = PCMK__PARSE_INT_DEFAULT;
+ size_t num_len = 0;
+ char *end_text = NULL;
+
+ if (input == NULL) {
+ return PCMK__PARSE_INT_DEFAULT;
+ }
+
+ num_start = input + strspn(input, WHITESPACE);
+ num_len = strspn(num_start, NUMCHARS);
+ if (num_len < 1) {
+ return PCMK__PARSE_INT_DEFAULT;
+ }
+ units = num_start + num_len;
+ units += strspn(units, WHITESPACE);
+
+ if (!strncasecmp(units, "ms", 2) || !strncasecmp(units, "msec", 4)) {
+ multiplier = 1;
+ divisor = 1;
+ } else if (!strncasecmp(units, "us", 2) || !strncasecmp(units, "usec", 4)) {
+ multiplier = 1;
+ divisor = 1000;
+ } else if (!strncasecmp(units, "s", 1) || !strncasecmp(units, "sec", 3)) {
+ multiplier = 1000;
+ divisor = 1;
+ } else if (!strncasecmp(units, "m", 1) || !strncasecmp(units, "min", 3)) {
+ multiplier = 60 * 1000;
+ divisor = 1;
+ } else if (!strncasecmp(units, "h", 1) || !strncasecmp(units, "hr", 2)) {
+ multiplier = 60 * 60 * 1000;
+ divisor = 1;
+ } else if ((*units != '\0') && (*units != '\n') && (*units != '\r')) {
+ return PCMK__PARSE_INT_DEFAULT;
+ }
+
+ scan_ll(num_start, &msec, PCMK__PARSE_INT_DEFAULT, &end_text);
+ if (msec > (LLONG_MAX / multiplier)) {
+ // Arithmetics overflow while multiplier/divisor mutually exclusive
+ return LLONG_MAX;
+ }
+ msec *= multiplier;
+ msec /= divisor;
+ return msec;
+}
+
+gboolean
+crm_is_true(const char *s)
+{
+ gboolean ret = FALSE;
+
+ if (s != NULL) {
+ crm_str_to_boolean(s, &ret);
+ }
+ return ret;
+}
+
+int
+crm_str_to_boolean(const char *s, int *ret)
+{
+ if (s == NULL) {
+ return -1;
+
+ } else if (strcasecmp(s, "true") == 0
+ || strcasecmp(s, "on") == 0
+ || strcasecmp(s, "yes") == 0 || strcasecmp(s, "y") == 0 || strcasecmp(s, "1") == 0) {
+ *ret = TRUE;
+ return 1;
+
+ } else if (strcasecmp(s, "false") == 0
+ || strcasecmp(s, "off") == 0
+ || strcasecmp(s, "no") == 0 || strcasecmp(s, "n") == 0 || strcasecmp(s, "0") == 0) {
+ *ret = FALSE;
+ return 1;
+ }
+ return -1;
+}
+
+/*!
+ * \internal
+ * \brief Replace any trailing newlines in a string with \0's
+ *
+ * \param[in,out] str String to trim
+ *
+ * \return \p str
+ */
+char *
+pcmk__trim(char *str)
+{
+ int len;
+
+ if (str == NULL) {
+ return str;
+ }
+
+ for (len = strlen(str) - 1; len >= 0 && str[len] == '\n'; len--) {
+ str[len] = '\0';
+ }
+
+ return str;
+}
+
+/*!
+ * \brief Check whether a string starts with a certain sequence
+ *
+ * \param[in] str String to check
+ * \param[in] prefix Sequence to match against beginning of \p str
+ *
+ * \return \c true if \p str begins with match, \c false otherwise
+ * \note This is equivalent to !strncmp(s, prefix, strlen(prefix))
+ * but is likely less efficient when prefix is a string literal
+ * if the compiler optimizes away the strlen() at compile time,
+ * and more efficient otherwise.
+ */
+bool
+pcmk__starts_with(const char *str, const char *prefix)
+{
+ const char *s = str;
+ const char *p = prefix;
+
+ if (!s || !p) {
+ return false;
+ }
+ while (*s && *p) {
+ if (*s++ != *p++) {
+ return false;
+ }
+ }
+ return (*p == 0);
+}
+
+static inline bool
+ends_with(const char *s, const char *match, bool as_extension)
+{
+ if (pcmk__str_empty(match)) {
+ return true;
+ } else if (s == NULL) {
+ return false;
+ } else {
+ size_t slen, mlen;
+
+ /* Besides as_extension, we could also check
+ !strchr(&match[1], match[0]) but that would be inefficient.
+ */
+ if (as_extension) {
+ s = strrchr(s, match[0]);
+ return (s == NULL)? false : !strcmp(s, match);
+ }
+
+ mlen = strlen(match);
+ slen = strlen(s);
+ return ((slen >= mlen) && !strcmp(s + slen - mlen, match));
+ }
+}
+
+/*!
+ * \internal
+ * \brief Check whether a string ends with a certain sequence
+ *
+ * \param[in] s String to check
+ * \param[in] match Sequence to match against end of \p s
+ *
+ * \return \c true if \p s ends case-sensitively with match, \c false otherwise
+ * \note pcmk__ends_with_ext() can be used if the first character of match
+ * does not recur in match.
+ */
+bool
+pcmk__ends_with(const char *s, const char *match)
+{
+ return ends_with(s, match, false);
+}
+
+/*!
+ * \internal
+ * \brief Check whether a string ends with a certain "extension"
+ *
+ * \param[in] s String to check
+ * \param[in] match Extension to match against end of \p s, that is,
+ * its first character must not occur anywhere
+ * in the rest of that very sequence (example: file
+ * extension where the last dot is its delimiter,
+ * e.g., ".html"); incorrect results may be
+ * returned otherwise.
+ *
+ * \return \c true if \p s ends (verbatim, i.e., case sensitively)
+ * with "extension" designated as \p match (including empty
+ * string), \c false otherwise
+ *
+ * \note Main incentive to prefer this function over \c pcmk__ends_with()
+ * where possible is the efficiency (at the cost of added
+ * restriction on \p match as stated; the complexity class
+ * remains the same, though: BigO(M+N) vs. BigO(M+2N)).
+ */
+bool
+pcmk__ends_with_ext(const char *s, const char *match)
+{
+ return ends_with(s, match, true);
+}
+
+/*!
+ * \internal
+ * \brief Create a hash of a string suitable for use with GHashTable
+ *
+ * \param[in] v String to hash
+ *
+ * \return A hash of \p v compatible with g_str_hash() before glib 2.28
+ * \note glib changed their hash implementation:
+ *
+ * https://gitlab.gnome.org/GNOME/glib/commit/354d655ba8a54b754cb5a3efb42767327775696c
+ *
+ * Note that the new g_str_hash is presumably a *better* hash (it's actually
+ * a correct implementation of DJB's hash), but we need to preserve existing
+ * behaviour, because the hash key ultimately determines the "sort" order
+ * when iterating through GHashTables, which affects allocation of scores to
+ * clone instances when iterating through rsc->allowed_nodes. It (somehow)
+ * also appears to have some minor impact on the ordering of a few
+ * pseudo_event IDs in the transition graph.
+ */
+static guint
+pcmk__str_hash(gconstpointer v)
+{
+ const signed char *p;
+ guint32 h = 0;
+
+ for (p = v; *p != '\0'; p++)
+ h = (h << 5) - h + *p;
+
+ return h;
+}
+
+/*!
+ * \internal
+ * \brief Create a hash table with case-sensitive strings as keys
+ *
+ * \param[in] key_destroy_func Function to free a key
+ * \param[in] value_destroy_func Function to free a value
+ *
+ * \return Newly allocated hash table
+ * \note It is the caller's responsibility to free the result, using
+ * g_hash_table_destroy().
+ */
+GHashTable *
+pcmk__strkey_table(GDestroyNotify key_destroy_func,
+ GDestroyNotify value_destroy_func)
+{
+ return g_hash_table_new_full(pcmk__str_hash, g_str_equal,
+ key_destroy_func, value_destroy_func);
+}
+
+/* used with hash tables where case does not matter */
+static gboolean
+pcmk__strcase_equal(gconstpointer a, gconstpointer b)
+{
+ return pcmk__str_eq((const char *)a, (const char *)b, pcmk__str_casei);
+}
+
+static guint
+pcmk__strcase_hash(gconstpointer v)
+{
+ const signed char *p;
+ guint32 h = 0;
+
+ for (p = v; *p != '\0'; p++)
+ h = (h << 5) - h + g_ascii_tolower(*p);
+
+ return h;
+}
+
+/*!
+ * \internal
+ * \brief Create a hash table with case-insensitive strings as keys
+ *
+ * \param[in] key_destroy_func Function to free a key
+ * \param[in] value_destroy_func Function to free a value
+ *
+ * \return Newly allocated hash table
+ * \note It is the caller's responsibility to free the result, using
+ * g_hash_table_destroy().
+ */
+GHashTable *
+pcmk__strikey_table(GDestroyNotify key_destroy_func,
+ GDestroyNotify value_destroy_func)
+{
+ return g_hash_table_new_full(pcmk__strcase_hash, pcmk__strcase_equal,
+ key_destroy_func, value_destroy_func);
+}
+
+static void
+copy_str_table_entry(gpointer key, gpointer value, gpointer user_data)
+{
+ if (key && value && user_data) {
+ g_hash_table_insert((GHashTable*)user_data, strdup(key), strdup(value));
+ }
+}
+
+/*!
+ * \internal
+ * \brief Copy a hash table that uses dynamically allocated strings
+ *
+ * \param[in,out] old_table Hash table to duplicate
+ *
+ * \return New hash table with copies of everything in \p old_table
+ * \note This assumes the hash table uses dynamically allocated strings -- that
+ * is, both the key and value free functions are free().
+ */
+GHashTable *
+pcmk__str_table_dup(GHashTable *old_table)
+{
+ GHashTable *new_table = NULL;
+
+ if (old_table) {
+ new_table = pcmk__strkey_table(free, free);
+ g_hash_table_foreach(old_table, copy_str_table_entry, new_table);
+ }
+ return new_table;
+}
+
+/*!
+ * \internal
+ * \brief Add a word to a string list of words
+ *
+ * \param[in,out] list Pointer to current string list (may not be \p NULL)
+ * \param[in] init_size \p list will be initialized to at least this size,
+ * if it needs initialization (if 0, use GLib's default
+ * initial string size)
+ * \param[in] word String to add to \p list (\p list will be
+ * unchanged if this is \p NULL or the empty string)
+ * \param[in] separator String to separate words in \p list
+ * (a space will be used if this is NULL)
+ *
+ * \note \p word may contain \p separator, though that would be a bad idea if
+ * the string needs to be parsed later.
+ */
+void
+pcmk__add_separated_word(GString **list, size_t init_size, const char *word,
+ const char *separator)
+{
+ CRM_ASSERT(list != NULL);
+
+ if (pcmk__str_empty(word)) {
+ return;
+ }
+
+ if (*list == NULL) {
+ if (init_size > 0) {
+ *list = g_string_sized_new(init_size);
+ } else {
+ *list = g_string_new(NULL);
+ }
+ }
+
+ if ((*list)->len == 0) {
+ // Don't add a separator before the first word in the list
+ separator = "";
+
+ } else if (separator == NULL) {
+ // Default to space-separated
+ separator = " ";
+ }
+
+ g_string_append(*list, separator);
+ g_string_append(*list, word);
+}
+
+/*!
+ * \internal
+ * \brief Compress data
+ *
+ * \param[in] data Data to compress
+ * \param[in] length Number of characters of data to compress
+ * \param[in] max Maximum size of compressed data (or 0 to estimate)
+ * \param[out] result Where to store newly allocated compressed result
+ * \param[out] result_len Where to store actual compressed length of result
+ *
+ * \return Standard Pacemaker return code
+ */
+int
+pcmk__compress(const char *data, unsigned int length, unsigned int max,
+ char **result, unsigned int *result_len)
+{
+ int rc;
+ char *compressed = NULL;
+ char *uncompressed = strdup(data);
+#ifdef CLOCK_MONOTONIC
+ struct timespec after_t;
+ struct timespec before_t;
+#endif
+
+ if (max == 0) {
+ max = (length * 1.01) + 601; // Size guaranteed to hold result
+ }
+
+#ifdef CLOCK_MONOTONIC
+ clock_gettime(CLOCK_MONOTONIC, &before_t);
+#endif
+
+ compressed = calloc((size_t) max, sizeof(char));
+ CRM_ASSERT(compressed);
+
+ *result_len = max;
+ rc = BZ2_bzBuffToBuffCompress(compressed, result_len, uncompressed, length,
+ CRM_BZ2_BLOCKS, 0, CRM_BZ2_WORK);
+ free(uncompressed);
+ if (rc != BZ_OK) {
+ crm_err("Compression of %d bytes failed: %s " CRM_XS " bzerror=%d",
+ length, bz2_strerror(rc), rc);
+ free(compressed);
+ return pcmk_rc_error;
+ }
+
+#ifdef CLOCK_MONOTONIC
+ clock_gettime(CLOCK_MONOTONIC, &after_t);
+
+ crm_trace("Compressed %d bytes into %d (ratio %d:1) in %.0fms",
+ length, *result_len, length / (*result_len),
+ (after_t.tv_sec - before_t.tv_sec) * 1000 +
+ (after_t.tv_nsec - before_t.tv_nsec) / 1e6);
+#else
+ crm_trace("Compressed %d bytes into %d (ratio %d:1)",
+ length, *result_len, length / (*result_len));
+#endif
+
+ *result = compressed;
+ return pcmk_rc_ok;
+}
+
+char *
+crm_strdup_printf(char const *format, ...)
+{
+ va_list ap;
+ int len = 0;
+ char *string = NULL;
+
+ va_start(ap, format);
+ len = vasprintf (&string, format, ap);
+ CRM_ASSERT(len > 0);
+ va_end(ap);
+ return string;
+}
+
+int
+pcmk__parse_ll_range(const char *srcstring, long long *start, long long *end)
+{
+ char *remainder = NULL;
+ int rc = pcmk_rc_ok;
+
+ CRM_ASSERT(start != NULL && end != NULL);
+
+ *start = PCMK__PARSE_INT_DEFAULT;
+ *end = PCMK__PARSE_INT_DEFAULT;
+
+ crm_trace("Attempting to decode: [%s]", srcstring);
+ if (pcmk__str_eq(srcstring, "", pcmk__str_null_matches)) {
+ return ENODATA;
+ } else if (pcmk__str_eq(srcstring, "-", pcmk__str_none)) {
+ return pcmk_rc_bad_input;
+ }
+
+ /* String starts with a dash, so this is either a range with
+ * no beginning or garbage.
+ * */
+ if (*srcstring == '-') {
+ int rc = scan_ll(srcstring+1, end, PCMK__PARSE_INT_DEFAULT, &remainder);
+
+ if (rc != pcmk_rc_ok || *remainder != '\0') {
+ return pcmk_rc_bad_input;
+ } else {
+ return pcmk_rc_ok;
+ }
+ }
+
+ rc = scan_ll(srcstring, start, PCMK__PARSE_INT_DEFAULT, &remainder);
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ }
+
+ if (*remainder && *remainder == '-') {
+ if (*(remainder+1)) {
+ char *more_remainder = NULL;
+ int rc = scan_ll(remainder+1, end, PCMK__PARSE_INT_DEFAULT,
+ &more_remainder);
+
+ if (rc != pcmk_rc_ok) {
+ return rc;
+ } else if (*more_remainder != '\0') {
+ return pcmk_rc_bad_input;
+ }
+ }
+ } else if (*remainder && *remainder != '-') {
+ *start = PCMK__PARSE_INT_DEFAULT;
+ return pcmk_rc_bad_input;
+ } else {
+ /* The input string contained only one number. Set start and end
+ * to the same value and return pcmk_rc_ok. This gives the caller
+ * a way to tell this condition apart from a range with no end.
+ */
+ *end = *start;
+ }
+
+ return pcmk_rc_ok;
+}
+
+/*!
+ * \internal
+ * \brief Find a string in a list of strings
+ *
+ * \note This function takes the same flags and has the same behavior as
+ * pcmk__str_eq().
+ *
+ * \note No matter what input string or flags are provided, an empty
+ * list will always return FALSE.
+ *
+ * \param[in] s String to search for
+ * \param[in] lst List to search
+ * \param[in] flags A bitfield of pcmk__str_flags to modify operation
+ *
+ * \return \c TRUE if \p s is in \p lst, or \c FALSE otherwise
+ */
+gboolean
+pcmk__str_in_list(const gchar *s, const GList *lst, uint32_t flags)
+{
+ for (const GList *ele = lst; ele != NULL; ele = ele->next) {
+ if (pcmk__str_eq(s, ele->data, flags)) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static bool
+str_any_of(const char *s, va_list args, uint32_t flags)
+{
+ if (s == NULL) {
+ return pcmk_is_set(flags, pcmk__str_null_matches);
+ }
+
+ while (1) {
+ const char *ele = va_arg(args, const char *);
+
+ if (ele == NULL) {
+ break;
+ } else if (pcmk__str_eq(s, ele, flags)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*!
+ * \internal
+ * \brief Is a string a member of a list of strings?
+ *
+ * \param[in] s String to search for in \p ...
+ * \param[in] ... Strings to compare \p s against. The final string
+ * must be NULL.
+ *
+ * \note The comparison is done case-insensitively. The function name is
+ * meant to be reminiscent of strcasecmp.
+ *
+ * \return \c true if \p s is in \p ..., or \c false otherwise
+ */
+bool
+pcmk__strcase_any_of(const char *s, ...)
+{
+ va_list ap;
+ bool rc;
+
+ va_start(ap, s);
+ rc = str_any_of(s, ap, pcmk__str_casei);
+ va_end(ap);
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Is a string a member of a list of strings?
+ *
+ * \param[in] s String to search for in \p ...
+ * \param[in] ... Strings to compare \p s against. The final string
+ * must be NULL.
+ *
+ * \note The comparison is done taking case into account.
+ *
+ * \return \c true if \p s is in \p ..., or \c false otherwise
+ */
+bool
+pcmk__str_any_of(const char *s, ...)
+{
+ va_list ap;
+ bool rc;
+
+ va_start(ap, s);
+ rc = str_any_of(s, ap, pcmk__str_none);
+ va_end(ap);
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Check whether a character is in any of a list of strings
+ *
+ * \param[in] ch Character (ASCII) to search for
+ * \param[in] ... Strings to search. Final argument must be
+ * \c NULL.
+ *
+ * \return \c true if any of \p ... contain \p ch, \c false otherwise
+ * \note \p ... must contain at least one argument (\c NULL).
+ */
+bool
+pcmk__char_in_any_str(int ch, ...)
+{
+ bool rc = false;
+ va_list ap;
+
+ /*
+ * Passing a char to va_start() can generate compiler warnings,
+ * so ch is declared as an int.
+ */
+ va_start(ap, ch);
+
+ while (1) {
+ const char *ele = va_arg(ap, const char *);
+
+ if (ele == NULL) {
+ break;
+ } else if (strchr(ele, ch) != NULL) {
+ rc = true;
+ break;
+ }
+ }
+
+ va_end(ap);
+ return rc;
+}
+
+/*!
+ * \internal
+ * \brief Sort strings, with numeric portions sorted numerically
+ *
+ * Sort two strings case-insensitively like strcasecmp(), but with any numeric
+ * portions of the string sorted numerically. This is particularly useful for
+ * node names (for example, "node10" will sort higher than "node9" but lower
+ * than "remotenode9").
+ *
+ * \param[in] s1 First string to compare (must not be NULL)
+ * \param[in] s2 Second string to compare (must not be NULL)
+ *
+ * \retval -1 \p s1 comes before \p s2
+ * \retval 0 \p s1 and \p s2 are equal
+ * \retval 1 \p s1 comes after \p s2
+ */
+int
+pcmk__numeric_strcasecmp(const char *s1, const char *s2)
+{
+ CRM_ASSERT((s1 != NULL) && (s2 != NULL));
+
+ while (*s1 && *s2) {
+ if (isdigit(*s1) && isdigit(*s2)) {
+ // If node names contain a number, sort numerically
+
+ char *end1 = NULL;
+ char *end2 = NULL;
+ long num1 = strtol(s1, &end1, 10);
+ long num2 = strtol(s2, &end2, 10);
+
+ // allow ordering e.g. 007 > 7
+ size_t len1 = end1 - s1;
+ size_t len2 = end2 - s2;
+
+ if (num1 < num2) {
+ return -1;
+ } else if (num1 > num2) {
+ return 1;
+ } else if (len1 < len2) {
+ return -1;
+ } else if (len1 > len2) {
+ return 1;
+ }
+ s1 = end1;
+ s2 = end2;
+ } else {
+ // Compare non-digits case-insensitively
+ int lower1 = tolower(*s1);
+ int lower2 = tolower(*s2);
+
+ if (lower1 < lower2) {
+ return -1;
+ } else if (lower1 > lower2) {
+ return 1;
+ }
+ ++s1;
+ ++s2;
+ }
+ }
+ if (!*s1 && *s2) {
+ return -1;
+ } else if (*s1 && !*s2) {
+ return 1;
+ }
+ return 0;
+}
+
+/*!
+ * \internal
+ * \brief Sort strings.
+ *
+ * This is your one-stop function for string comparison. By default, this
+ * function works like \p g_strcmp0. That is, like \p strcmp but a \p NULL
+ * string sorts before a non-<tt>NULL</tt> string.
+ *
+ * The \p pcmk__str_none flag produces the default behavior. Behavior can be
+ * changed with various flags:
+ *
+ * - \p pcmk__str_regex - The second string is a regular expression that the
+ * first string will be matched against.
+ * - \p pcmk__str_casei - By default, comparisons are done taking case into
+ * account. This flag makes comparisons case-
+ * insensitive. This can be combined with
+ * \p pcmk__str_regex.
+ * - \p pcmk__str_null_matches - If one string is \p NULL and the other is not,
+ * still return \p 0.
+ * - \p pcmk__str_star_matches - If one string is \p "*" and the other is not,
+ * still return \p 0.
+ *
+ * \param[in] s1 First string to compare
+ * \param[in] s2 Second string to compare, or a regular expression to
+ * match if \p pcmk__str_regex is set
+ * \param[in] flags A bitfield of \p pcmk__str_flags to modify operation
+ *
+ * \retval negative \p s1 is \p NULL or comes before \p s2
+ * \retval 0 \p s1 and \p s2 are equal, or \p s1 is found in \p s2 if
+ * \c pcmk__str_regex is set
+ * \retval positive \p s2 is \p NULL or \p s1 comes after \p s2, or \p s2
+ * is an invalid regular expression, or \p s1 was not found
+ * in \p s2 if \p pcmk__str_regex is set.
+ */
+int
+pcmk__strcmp(const char *s1, const char *s2, uint32_t flags)
+{
+ /* If this flag is set, the second string is a regex. */
+ if (pcmk_is_set(flags, pcmk__str_regex)) {
+ regex_t r_patt;
+ int reg_flags = REG_EXTENDED | REG_NOSUB;
+ int regcomp_rc = 0;
+ int rc = 0;
+
+ if (s1 == NULL || s2 == NULL) {
+ return 1;
+ }
+
+ if (pcmk_is_set(flags, pcmk__str_casei)) {
+ reg_flags |= REG_ICASE;
+ }
+ regcomp_rc = regcomp(&r_patt, s2, reg_flags);
+ if (regcomp_rc != 0) {
+ rc = 1;
+ crm_err("Bad regex '%s' for update: %s", s2, strerror(regcomp_rc));
+ } else {
+ rc = regexec(&r_patt, s1, 0, NULL, 0);
+ regfree(&r_patt);
+ if (rc != 0) {
+ rc = 1;
+ }
+ }
+ return rc;
+ }
+
+ /* If the strings are the same pointer, return 0 immediately. */
+ if (s1 == s2) {
+ return 0;
+ }
+
+ /* If this flag is set, return 0 if either (or both) of the input strings
+ * are NULL. If neither one is NULL, we need to continue and compare
+ * them normally.
+ */
+ if (pcmk_is_set(flags, pcmk__str_null_matches)) {
+ if (s1 == NULL || s2 == NULL) {
+ return 0;
+ }
+ }
+
+ /* Handle the cases where one is NULL and the str_null_matches flag is not set.
+ * A NULL string always sorts to the beginning.
+ */
+ if (s1 == NULL) {
+ return -1;
+ } else if (s2 == NULL) {
+ return 1;
+ }
+
+ /* If this flag is set, return 0 if either (or both) of the input strings
+ * are "*". If neither one is, we need to continue and compare them
+ * normally.
+ */
+ if (pcmk_is_set(flags, pcmk__str_star_matches)) {
+ if (strcmp(s1, "*") == 0 || strcmp(s2, "*") == 0) {
+ return 0;
+ }
+ }
+
+ if (pcmk_is_set(flags, pcmk__str_casei)) {
+ return strcasecmp(s1, s2);
+ } else {
+ return strcmp(s1, s2);
+ }
+}
+
+/*!
+ * \internal
+ * \brief Update a dynamically allocated string with a new value
+ *
+ * Given a dynamically allocated string and a new value for it, if the string
+ * is different from the new value, free the string and replace it with either a
+ * newly allocated duplicate of the value or NULL as appropriate.
+ *
+ * \param[in,out] str Pointer to dynamically allocated string
+ * \param[in] value New value to duplicate (or NULL)
+ *
+ * \note The caller remains responsibile for freeing \p *str.
+ */
+void
+pcmk__str_update(char **str, const char *value)
+{
+ if ((str != NULL) && !pcmk__str_eq(*str, value, pcmk__str_none)) {
+ free(*str);
+ if (value == NULL) {
+ *str = NULL;
+ } else {
+ *str = strdup(value);
+ CRM_ASSERT(*str != NULL);
+ }
+ }
+}
+
+/*!
+ * \internal
+ * \brief Append a list of strings to a destination \p GString
+ *
+ * \param[in,out] buffer Where to append the strings (must not be \p NULL)
+ * \param[in] ... A <tt>NULL</tt>-terminated list of strings
+ *
+ * \note This tends to be more efficient than a single call to
+ * \p g_string_append_printf().
+ */
+void
+pcmk__g_strcat(GString *buffer, ...)
+{
+ va_list ap;
+
+ CRM_ASSERT(buffer != NULL);
+ va_start(ap, buffer);
+
+ while (true) {
+ const char *ele = va_arg(ap, const char *);
+
+ if (ele == NULL) {
+ break;
+ }
+ g_string_append(buffer, ele);
+ }
+ va_end(ap);
+}
+
+// Deprecated functions kept only for backward API compatibility
+// LCOV_EXCL_START
+
+#include <crm/common/util_compat.h>
+
+gboolean
+safe_str_neq(const char *a, const char *b)
+{
+ if (a == b) {
+ return FALSE;
+
+ } else if (a == NULL || b == NULL) {
+ return TRUE;
+
+ } else if (strcasecmp(a, b) == 0) {
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+crm_str_eq(const char *a, const char *b, gboolean use_case)
+{
+ if (use_case) {
+ return g_strcmp0(a, b) == 0;
+
+ /* TODO - Figure out which calls, if any, really need to be case independent */
+ } else if (a == b) {
+ return TRUE;
+
+ } else if (a == NULL || b == NULL) {
+ /* shouldn't be comparing NULLs */
+ return FALSE;
+
+ } else if (strcasecmp(a, b) == 0) {
+ return TRUE;
+ }
+ return FALSE;
+}
+
+char *
+crm_itoa_stack(int an_int, char *buffer, size_t len)
+{
+ if (buffer != NULL) {
+ snprintf(buffer, len, "%d", an_int);
+ }
+ return buffer;
+}
+
+guint
+g_str_hash_traditional(gconstpointer v)
+{
+ return pcmk__str_hash(v);
+}
+
+gboolean
+crm_strcase_equal(gconstpointer a, gconstpointer b)
+{
+ return pcmk__strcase_equal(a, b);
+}
+
+guint
+crm_strcase_hash(gconstpointer v)
+{
+ return pcmk__strcase_hash(v);
+}
+
+GHashTable *
+crm_str_table_dup(GHashTable *old_table)
+{
+ return pcmk__str_table_dup(old_table);
+}
+
+long long
+crm_parse_ll(const char *text, const char *default_text)
+{
+ long long result;
+
+ if (text == NULL) {
+ text = default_text;
+ if (text == NULL) {
+ crm_err("No default conversion value supplied");
+ errno = EINVAL;
+ return PCMK__PARSE_INT_DEFAULT;
+ }
+ }
+ scan_ll(text, &result, PCMK__PARSE_INT_DEFAULT, NULL);
+ return result;
+}
+
+int
+crm_parse_int(const char *text, const char *default_text)
+{
+ long long result = crm_parse_ll(text, default_text);
+
+ if (result < INT_MIN) {
+ // If errno is ERANGE, crm_parse_ll() has already logged a message
+ if (errno != ERANGE) {
+ crm_err("Conversion of %s was clipped: %lld", text, result);
+ errno = ERANGE;
+ }
+ return INT_MIN;
+
+ } else if (result > INT_MAX) {
+ // If errno is ERANGE, crm_parse_ll() has already logged a message
+ if (errno != ERANGE) {
+ crm_err("Conversion of %s was clipped: %lld", text, result);
+ errno = ERANGE;
+ }
+ return INT_MAX;
+ }
+
+ return (int) result;
+}
+
+char *
+crm_strip_trailing_newline(char *str)
+{
+ return pcmk__trim(str);
+}
+
+int
+pcmk_numeric_strcasecmp(const char *s1, const char *s2)
+{
+ return pcmk__numeric_strcasecmp(s1, s2);
+}
+
+// LCOV_EXCL_STOP
+// End deprecated API