diff --git a/include/crm/common/Makefile.am b/include/crm/common/Makefile.am
index d63984d8e3..a211b82a00 100644
--- a/include/crm/common/Makefile.am
+++ b/include/crm/common/Makefile.am
@@ -1,45 +1,46 @@
 #
 # Copyright 2004-2024 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
 # This source code is licensed under the GNU General Public License version 2
 # or later (GPLv2+) WITHOUT ANY WARRANTY.
 #
 
 MAINTAINERCLEANFILES = Makefile.in
 
 headerdir=$(pkgincludedir)/crm/common
 
 header_HEADERS = acl.h 			\
 		 actions.h		\
 		 agents.h 		\
 		 cib.h 			\
 		 ipc.h 			\
 		 ipc_controld.h 	\
 		 ipc_pacemakerd.h 	\
 		 ipc_schedulerd.h 	\
 		 iso8601.h 		\
 		 logging.h 		\
 		 logging_compat.h 	\
 		 mainloop.h 		\
 		 nodes.h 		\
 		 nvpair.h 		\
 		 options.h 		\
 		 output.h 		\
 		 resources.h		\
 		 results.h 		\
 		 roles.h		\
 		 rules.h		\
 		 scheduler.h		\
 		 scheduler_types.h	\
 		 schemas.h		\
 		 scores.h		\
+		 strings.h		\
 		 util.h 		\
 		 util_compat.h 		\
 		 xml.h 			\
 		 xml_compat.h		\
 		 xml_io.h		\
 		 xml_names.h
 
 noinst_HEADERS = $(wildcard *internal.h)
diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h
index 15c72805eb..b20b66e896 100644
--- a/include/crm/common/internal.h
+++ b/include/crm/common/internal.h
@@ -1,400 +1,400 @@
 /*
  * Copyright 2015-2024 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.
  */
 
 #ifndef PCMK__CRM_COMMON_INTERNAL__H
 #define PCMK__CRM_COMMON_INTERNAL__H
 
 #include <unistd.h>             // pid_t, getpid()
 #include <stdbool.h>            // bool
 #include <stdint.h>             // uint8_t, uint64_t
 
 #include <glib.h>               // guint, GList, GHashTable
 #include <libxml/tree.h>        // xmlNode
 
-#include <crm/common/util.h>    // crm_strdup_printf()
 #include <crm/common/logging.h>  // do_crm_log_unlikely(), etc.
 #include <crm/common/mainloop.h> // mainloop_io_t, struct ipc_client_callbacks
+#include <crm/common/strings.h>  // crm_strdup_printf()
 #include <crm/common/actions_internal.h>
 #include <crm/common/digest_internal.h>
 #include <crm/common/health_internal.h>
 #include <crm/common/io_internal.h>
 #include <crm/common/iso8601_internal.h>
 #include <crm/common/results_internal.h>
 #include <crm/common/messages_internal.h>
 #include <crm/common/nvpair_internal.h>
 #include <crm/common/scores_internal.h>
 #include <crm/common/strings_internal.h>
 #include <crm/common/acl_internal.h>
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /* This says whether the current application is a Pacemaker daemon or not,
  * and is used to change default logging settings such as whether to log to
  * stderr, etc., as well as a few other details such as whether blackbox signal
  * handling is enabled.
  *
  * It is set when logging is initialized, and does not need to be set directly.
  */
 extern bool pcmk__is_daemon;
 
 // Number of elements in a statically defined array
 #define PCMK__NELEM(a) ((int) (sizeof(a)/sizeof(a[0])) )
 
 #if SUPPORT_CIBSECRETS
 /* internal CIB utilities (from cib_secrets.c) */
 
 int pcmk__substitute_secrets(const char *rsc_id, GHashTable *params);
 #endif
 
 
 /* internal main loop utilities (from mainloop.c) */
 
 int pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata,
                            const struct ipc_client_callbacks *callbacks,
                            mainloop_io_t **source);
 guint pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer);
 
 
 /* internal node-related XML utilities (from nodes.c) */
 
 /*!
  * \internal
  * \brief Add local node name and ID to an XML node
  *
  * \param[in,out] request  XML node to modify
  * \param[in]     node     The local node's name
  * \param[in]     nodeid   The local node's ID (can be 0)
  */
 void pcmk__xe_add_node(xmlNode *xml, const char *node, int nodeid);
 
 
 /* internal name/value utilities (from nvpair.c) */
 
 int pcmk__scan_nvpair(const char *input, char **name, char **value);
 char *pcmk__format_nvpair(const char *name, const char *value,
                           const char *units);
 
 /*!
  * \internal
  * \brief Add a boolean attribute to an XML node.
  *
  * \param[in,out] node  XML node to add attributes to
  * \param[in]     name  XML attribute to create
  * \param[in]     value Value to give to the attribute
  */
 void
 pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value);
 
 /*!
  * \internal
  * \brief Extract a boolean attribute's value from an XML element
  *
  * \param[in] node XML node to get attribute from
  * \param[in] name XML attribute to get
  *
  * \return True if the given \p name is an attribute on \p node and has
  *         the value \c PCMK_VALUE_TRUE, False in all other cases
  */
 bool
 pcmk__xe_attr_is_true(const xmlNode *node, const char *name);
 
 /*!
  * \internal
  * \brief Extract a boolean attribute's value from an XML element, with
  *        error checking
  *
  * \param[in]  node  XML node to get attribute from
  * \param[in]  name  XML attribute to get
  * \param[out] value Destination for the value of the attribute
  *
  * \return EINVAL if \p name or \p value are NULL, ENODATA if \p node is
  *         NULL or the attribute does not exist, pcmk_rc_unknown_format
  *         if the attribute is not a boolean, and pcmk_rc_ok otherwise.
  *
  * \note \p value only has any meaning if the return value is pcmk_rc_ok.
  */
 int
 pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value);
 
 
 /* internal procfs utilities (from procfs.c) */
 
 pid_t pcmk__procfs_pid_of(const char *name);
 unsigned int pcmk__procfs_num_cores(void);
 int pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size);
 bool pcmk__procfs_has_pids(void);
 
 /* internal functions related to process IDs (from pid.c) */
 
 /*!
  * \internal
  * \brief Check whether process exists (by PID and optionally executable path)
  *
  * \param[in] pid     PID of process to check
  * \param[in] daemon  If not NULL, path component to match with procfs entry
  *
  * \return Standard Pacemaker return code
  * \note Particular return codes of interest include pcmk_rc_ok for alive,
  *       ESRCH for process is not alive (verified by kill and/or executable path
  *       match), EACCES for caller unable or not allowed to check. A result of
  *       "alive" is less reliable when \p daemon is not provided or procfs is
  *       not available, since there is no guarantee that the PID has not been
  *       recycled for another process.
  * \note This function cannot be used to verify \e authenticity of the process.
  */
 int pcmk__pid_active(pid_t pid, const char *daemon);
 
 int pcmk__read_pidfile(const char *filename, pid_t *pid);
 int pcmk__pidfile_matches(const char *filename, pid_t expected_pid,
                           const char *expected_name, pid_t *pid);
 int pcmk__lock_pidfile(const char *filename, const char *name);
 
 
 // bitwise arithmetic utilities
 
 /*!
  * \internal
  * \brief Set specified flags in a flag group
  *
  * \param[in] function    Function name of caller
  * \param[in] line        Line number of caller
  * \param[in] log_level   Log a message at this level
  * \param[in] flag_type   Label describing this flag group (for logging)
  * \param[in] target      Name of object whose flags these are (for logging)
  * \param[in] flag_group  Flag group being manipulated
  * \param[in] flags       Which flags in the group should be set
  * \param[in] flags_str   Readable equivalent of \p flags (for logging)
  *
  * \return Possibly modified flag group
  */
 static inline uint64_t
 pcmk__set_flags_as(const char *function, int line, uint8_t log_level,
                    const char *flag_type, const char *target,
                    uint64_t flag_group, uint64_t flags, const char *flags_str)
 {
     uint64_t result = flag_group | flags;
 
     if (result != flag_group) {
         do_crm_log_unlikely(log_level,
                             "%s flags %#.8llx (%s) for %s set by %s:%d",
                             ((flag_type == NULL)? "Group of" : flag_type),
                             (unsigned long long) flags,
                             ((flags_str == NULL)? "flags" : flags_str),
                             ((target == NULL)? "target" : target),
                             function, line);
     }
     return result;
 }
 
 /*!
  * \internal
  * \brief Clear specified flags in a flag group
  *
  * \param[in] function    Function name of caller
  * \param[in] line        Line number of caller
  * \param[in] log_level   Log a message at this level
  * \param[in] flag_type   Label describing this flag group (for logging)
  * \param[in] target      Name of object whose flags these are (for logging)
  * \param[in] flag_group  Flag group being manipulated
  * \param[in] flags       Which flags in the group should be cleared
  * \param[in] flags_str   Readable equivalent of \p flags (for logging)
  *
  * \return Possibly modified flag group
  */
 static inline uint64_t
 pcmk__clear_flags_as(const char *function, int line, uint8_t log_level,
                      const char *flag_type, const char *target,
                      uint64_t flag_group, uint64_t flags, const char *flags_str)
 {
     uint64_t result = flag_group & ~flags;
 
     if (result != flag_group) {
         do_crm_log_unlikely(log_level,
                             "%s flags %#.8llx (%s) for %s cleared by %s:%d",
                             ((flag_type == NULL)? "Group of" : flag_type),
                             (unsigned long long) flags,
                             ((flags_str == NULL)? "flags" : flags_str),
                             ((target == NULL)? "target" : target),
                             function, line);
     }
     return result;
 }
 
 /*!
  * \internal
  * \brief Get readable string for whether specified flags are set
  *
  * \param[in] flag_group    Group of flags to check
  * \param[in] flags         Which flags in \p flag_group should be checked
  *
  * \return "true" if all \p flags are set in \p flag_group, otherwise "false"
  */
 static inline const char *
 pcmk__flag_text(uint64_t flag_group, uint64_t flags)
 {
     return pcmk__btoa(pcmk_all_flags_set(flag_group, flags));
 }
 
 
 // miscellaneous utilities (from utils.c)
 
 void pcmk__daemonize(const char *name, const char *pidfile);
 void pcmk__panic(const char *origin);
 pid_t pcmk__locate_sbd(void);
 void pcmk__sleep_ms(unsigned int ms);
 
 extern int pcmk__score_red;
 extern int pcmk__score_green;
 extern int pcmk__score_yellow;
 
 /*!
  * \internal
  * \brief Allocate new zero-initialized memory, asserting on failure
  *
  * \param[in] file      File where \p function is located
  * \param[in] function  Calling function
  * \param[in] line      Line within \p file
  * \param[in] nmemb     Number of elements to allocate memory for
  * \param[in] size      Size of each element
  *
  * \return Newly allocated memory of of size <tt>nmemb * size</tt> (guaranteed
  *         not to be \c NULL)
  *
  * \note The caller is responsible for freeing the return value using \c free().
  */
 static inline void *
 pcmk__assert_alloc_as(const char *file, const char *function, uint32_t line,
                       size_t nmemb, size_t size)
 {
     void *ptr = calloc(nmemb, size);
 
     if (ptr == NULL) {
         crm_abort(file, function, line, "Out of memory", FALSE, TRUE);
         crm_exit(CRM_EX_OSERR);
     }
     return ptr;
 }
 
 /*!
  * \internal
  * \brief Allocate new zero-initialized memory, asserting on failure
  *
  * \param[in] nmemb  Number of elements to allocate memory for
  * \param[in] size   Size of each element
  *
  * \return Newly allocated memory of of size <tt>nmemb * size</tt> (guaranteed
  *         not to be \c NULL)
  *
  * \note The caller is responsible for freeing the return value using \c free().
  */
 #define pcmk__assert_alloc(nmemb, size) \
     pcmk__assert_alloc_as(__FILE__, __func__, __LINE__, nmemb, size)
 
 /*!
  * \internal
  * \brief Resize a dynamically allocated memory block
  *
  * \param[in] ptr   Memory block to resize (or NULL to allocate new memory)
  * \param[in] size  New size of memory block in bytes (must be > 0)
  *
  * \return Pointer to resized memory block
  *
  * \note This asserts on error, so the result is guaranteed to be non-NULL
  *       (which is the main advantage of this over directly using realloc()).
  */
 static inline void *
 pcmk__realloc(void *ptr, size_t size)
 {
     void *new_ptr;
 
     // realloc(p, 0) can replace free(p) but this wrapper can't
     CRM_ASSERT(size > 0);
 
     new_ptr = realloc(ptr, size);
     if (new_ptr == NULL) {
         free(ptr);
         abort();
     }
     return new_ptr;
 }
 
 static inline char *
 pcmk__getpid_s(void)
 {
     return crm_strdup_printf("%lu", (unsigned long) getpid());
 }
 
 // More efficient than g_list_length(list) == 1
 static inline bool
 pcmk__list_of_1(GList *list)
 {
     return list && (list->next == NULL);
 }
 
 // More efficient than g_list_length(list) > 1
 static inline bool
 pcmk__list_of_multiple(GList *list)
 {
     return list && (list->next != NULL);
 }
 
 /* convenience functions for failure-related node attributes */
 
 #define PCMK__FAIL_COUNT_PREFIX   "fail-count"
 #define PCMK__LAST_FAILURE_PREFIX "last-failure"
 
 /*!
  * \internal
  * \brief Generate a failure-related node attribute name for a resource
  *
  * \param[in] prefix       Start of attribute name
  * \param[in] rsc_id       Resource name
  * \param[in] op           Operation name
  * \param[in] interval_ms  Operation interval
  *
  * \return Newly allocated string with attribute name
  *
  * \note Failure attributes are named like PREFIX-RSC#OP_INTERVAL (for example,
  *       "fail-count-myrsc#monitor_30000"). The '#' is used because it is not
  *       a valid character in a resource ID, to reliably distinguish where the
  *       operation name begins. The '_' is used simply to be more comparable to
  *       action labels like "myrsc_monitor_30000".
  */
 static inline char *
 pcmk__fail_attr_name(const char *prefix, const char *rsc_id, const char *op,
                    guint interval_ms)
 {
     CRM_CHECK(prefix && rsc_id && op, return NULL);
     return crm_strdup_printf("%s-%s#%s_%u", prefix, rsc_id, op, interval_ms);
 }
 
 static inline char *
 pcmk__failcount_name(const char *rsc_id, const char *op, guint interval_ms)
 {
     return pcmk__fail_attr_name(PCMK__FAIL_COUNT_PREFIX, rsc_id, op,
                                 interval_ms);
 }
 
 static inline char *
 pcmk__lastfailure_name(const char *rsc_id, const char *op, guint interval_ms)
 {
     return pcmk__fail_attr_name(PCMK__LAST_FAILURE_PREFIX, rsc_id, op,
                                 interval_ms);
 }
 
 // internal resource agent functions (from agents.c)
 int pcmk__effective_rc(int rc);
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_INTERNAL__H
diff --git a/include/crm/common/options_internal.h b/include/crm/common/options_internal.h
index e165b213da..1b61057499 100644
--- a/include/crm/common/options_internal.h
+++ b/include/crm/common/options_internal.h
@@ -1,260 +1,260 @@
 /*
  * Copyright 2006-2024 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.
  */
 
 #ifndef PCMK__CRM_COMMON_OPTIONS_INTERNAL__H
 #define PCMK__CRM_COMMON_OPTIONS_INTERNAL__H
 
 #ifndef PCMK__CONFIG_H
 #define PCMK__CONFIG_H
 #include <config.h>   // _Noreturn
 #endif
 
 #include <glib.h>     // GHashTable
 #include <stdbool.h>  // bool
 
-#include <crm/common/util.h>                // pcmk_parse_interval_spec()
+#include <crm/common/strings.h>             // pcmk_parse_interval_spec()
 #include <crm/common/output_internal.h>     // pcmk__output_t
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 _Noreturn void pcmk__cli_help(char cmd);
 
 
 /*
  * Environment variable option handling
  */
 
 const char *pcmk__env_option(const char *option);
 void pcmk__set_env_option(const char *option, const char *value, bool compat);
 bool pcmk__env_option_enabled(const char *daemon, const char *option);
 
 
 /*
  * Cluster option handling
  */
 
 /*!
  * \internal
  * \enum pcmk__opt_flags
  * \brief Option flags
  */
 enum pcmk__opt_flags {
     pcmk__opt_none       = 0U,          //!< No additional information
 
     /*!
      * \brief In CIB manager metadata
      *
      * \deprecated This flag will be removed with CIB manager metadata
      */
     pcmk__opt_based      = (1U << 0),
 
     /*!
      * \brief In controller metadata
      *
      * \deprecated This flag will be removed with controller metadata
      */
     pcmk__opt_controld   = (1U << 1),
 
     /*!
      * \brief In scheduler metadata
      *
      * \deprecated This flag will be removed with scheduler metadata
      */
     pcmk__opt_schedulerd = (1U << 2),
 
     pcmk__opt_advanced   = (1U << 3),   //!< Advanced use only
     pcmk__opt_generated  = (1U << 4),   //!< Generated by Pacemaker
     pcmk__opt_deprecated = (1U << 5),   //!< Option is deprecated
     pcmk__opt_fencing    = (1U << 6),   //!< Common fencing resource parameter
     pcmk__opt_primitive  = (1U << 7),   //!< Primitive resource meta-attribute
 };
 
 typedef struct pcmk__cluster_option_s {
     const char *name;
     const char *alt_name;
     const char *type;
     const char *values;
     const char *default_value;
 
     bool (*is_valid)(const char *);
 
     uint32_t flags;                 //!< Group of <tt>enum pcmk__opt_flags</tt>
 
     const char *description_short;
     const char *description_long;
 
 } pcmk__cluster_option_t;
 
 const char *pcmk__cluster_option(GHashTable *options, const char *name);
 
 int pcmk__output_cluster_options(pcmk__output_t *out, const char *name,
                                  const char *desc_short, const char *desc_long,
                                  uint32_t filter, bool all);
 int pcmk__output_fencing_params(pcmk__output_t *out, const char *name,
                                 const char *desc_short, const char *desc_long,
                                 bool all);
 int pcmk__output_primitive_meta(pcmk__output_t *out, const char *name,
                                 const char *desc_short, const char *desc_long,
                                 bool all);
 
 int pcmk__daemon_metadata(pcmk__output_t *out, const char *name,
                           const char *short_desc, const char *long_desc,
                           enum pcmk__opt_flags filter);
 
 void pcmk__validate_cluster_options(GHashTable *options);
 
 bool pcmk__valid_interval_spec(const char *value);
 bool pcmk__valid_boolean(const char *value);
 bool pcmk__valid_int(const char *value);
 bool pcmk__valid_positive_int(const char *value);
 bool pcmk__valid_no_quorum_policy(const char *value);
 bool pcmk__valid_percentage(const char *value);
 bool pcmk__valid_placement_strategy(const char *value);
 
 // from watchdog.c
 long pcmk__get_sbd_watchdog_timeout(void);
 bool pcmk__get_sbd_sync_resource_startup(void);
 long pcmk__auto_stonith_watchdog_timeout(void);
 bool pcmk__valid_stonith_watchdog_timeout(const char *value);
 
 // Constants for environment variable names
 #define PCMK__ENV_AUTHKEY_LOCATION          "authkey_location"
 #define PCMK__ENV_BLACKBOX                  "blackbox"
 #define PCMK__ENV_CALLGRIND_ENABLED         "callgrind_enabled"
 #define PCMK__ENV_CLUSTER_TYPE              "cluster_type"
 #define PCMK__ENV_DEBUG                     "debug"
 #define PCMK__ENV_DH_MAX_BITS               "dh_max_bits"
 #define PCMK__ENV_FAIL_FAST                 "fail_fast"
 #define PCMK__ENV_IPC_BUFFER                "ipc_buffer"
 #define PCMK__ENV_IPC_TYPE                  "ipc_type"
 #define PCMK__ENV_LOGFACILITY               "logfacility"
 #define PCMK__ENV_LOGFILE                   "logfile"
 #define PCMK__ENV_LOGFILE_MODE              "logfile_mode"
 #define PCMK__ENV_LOGPRIORITY               "logpriority"
 #define PCMK__ENV_NODE_ACTION_LIMIT         "node_action_limit"
 #define PCMK__ENV_NODE_START_STATE          "node_start_state"
 #define PCMK__ENV_PANIC_ACTION              "panic_action"
 #define PCMK__ENV_REMOTE_ADDRESS            "remote_address"
 #define PCMK__ENV_REMOTE_SCHEMA_DIRECTORY   "remote_schema_directory"
 #define PCMK__ENV_REMOTE_PID1               "remote_pid1"
 #define PCMK__ENV_REMOTE_PORT               "remote_port"
 #define PCMK__ENV_RESPAWNED                 "respawned"
 #define PCMK__ENV_SCHEMA_DIRECTORY          "schema_directory"
 #define PCMK__ENV_SERVICE                   "service"
 #define PCMK__ENV_STDERR                    "stderr"
 #define PCMK__ENV_TLS_PRIORITIES            "tls_priorities"
 #define PCMK__ENV_TRACE_BLACKBOX            "trace_blackbox"
 #define PCMK__ENV_TRACE_FILES               "trace_files"
 #define PCMK__ENV_TRACE_FORMATS             "trace_formats"
 #define PCMK__ENV_TRACE_FUNCTIONS           "trace_functions"
 #define PCMK__ENV_TRACE_TAGS                "trace_tags"
 #define PCMK__ENV_VALGRIND_ENABLED          "valgrind_enabled"
 
 // @COMPAT Deprecated since 2.1.0
 #define PCMK__OPT_REMOVE_AFTER_STOP         "remove-after-stop"
 
 // Constants for meta-attribute names
 #define PCMK__META_CLONE                    "clone"
 #define PCMK__META_CONTAINER                "container"
 #define PCMK__META_DIGESTS_ALL              "digests-all"
 #define PCMK__META_DIGESTS_SECURE           "digests-secure"
 #define PCMK__META_INTERNAL_RSC             "internal_rsc"
 #define PCMK__META_MIGRATE_SOURCE           "migrate_source"
 #define PCMK__META_MIGRATE_TARGET           "migrate_target"
 #define PCMK__META_ON_NODE                  "on_node"
 #define PCMK__META_ON_NODE_UUID             "on_node_uuid"
 #define PCMK__META_OP_NO_WAIT               "op_no_wait"
 #define PCMK__META_OP_TARGET_RC             "op_target_rc"
 #define PCMK__META_PHYSICAL_HOST            "physical-host"
 #define PCMK__META_STONITH_ACTION           "stonith_action"
 
 /* @TODO Plug these in. Currently, they're never set. These are op attrs for use
  * with https://projects.clusterlabs.org/T382.
  */
 #define PCMK__META_CLEAR_FAILURE_OP         "clear_failure_op"
 #define PCMK__META_CLEAR_FAILURE_INTERVAL   "clear_failure_interval"
 
 // @COMPAT Deprecated meta-attribute since 2.1.0
 #define PCMK__META_CAN_FAIL                 "can_fail"
 
 // @COMPAT Deprecated alias for PCMK__META_PROMOTED_MAX since 2.0.0
 #define PCMK__META_PROMOTED_MAX_LEGACY      "master-max"
 
 // @COMPAT Deprecated alias for PCMK__META_PROMOTED_NODE_MAX since 2.0.0
 #define PCMK__META_PROMOTED_NODE_MAX_LEGACY "master-node-max"
 
 // @COMPAT Deprecated meta-attribute since 2.0.0
 #define PCMK__META_RESTART_TYPE             "restart-type"
 
 // @COMPAT Deprecated meta-attribute since 2.0.0
 #define PCMK__META_ROLE_AFTER_FAILURE       "role_after_failure"
 
 // Constants for enumerated values
 #define PCMK__VALUE_ATTRD                   "attrd"
 #define PCMK__VALUE_BOLD                    "bold"
 #define PCMK__VALUE_BROADCAST               "broadcast"
 #define PCMK__VALUE_CIB                     "cib"
 #define PCMK__VALUE_CIB_DIFF_NOTIFY         "cib_diff_notify"
 #define PCMK__VALUE_CIB_NOTIFY              "cib_notify"
 #define PCMK__VALUE_CIB_POST_NOTIFY         "cib_post_notify"
 #define PCMK__VALUE_CIB_PRE_NOTIFY          "cib_pre_notify"
 #define PCMK__VALUE_CIB_UPDATE_CONFIRMATION "cib_update_confirmation"
 #define PCMK__VALUE_CLUSTER                 "cluster"
 #define PCMK__VALUE_CRMD                    "crmd"
 #define PCMK__VALUE_EN                      "en"
 #define PCMK__VALUE_EPOCH                   "epoch"
 #define PCMK__VALUE_HEALTH_RED              "health_red"
 #define PCMK__VALUE_HEALTH_YELLOW           "health_yellow"
 #define PCMK__VALUE_INIT                    "init"
 #define PCMK__VALUE_LOCAL                   "local"
 #define PCMK__VALUE_LOST                    "lost"
 #define PCMK__VALUE_LRMD                    "lrmd"
 #define PCMK__VALUE_MAINT                   "maint"
 #define PCMK__VALUE_OUTPUT                  "output"
 #define PCMK__VALUE_PASSWORD                "password"
 #define PCMK__VALUE_PING                    "ping"
 #define PCMK__VALUE_PRIMITIVE               "primitive"
 #define PCMK__VALUE_REFRESH                 "refresh"
 #define PCMK__VALUE_REQUEST                 "request"
 #define PCMK__VALUE_RESPONSE                "response"
 #define PCMK__VALUE_RSC_FAILED              "rsc-failed"
 #define PCMK__VALUE_RSC_FAILURE_IGNORED     "rsc-failure-ignored"
 #define PCMK__VALUE_RSC_MANAGED             "rsc-managed"
 #define PCMK__VALUE_RSC_MULTIPLE            "rsc-multiple"
 #define PCMK__VALUE_RSC_OK                  "rsc-ok"
 #define PCMK__VALUE_RUNNING                 "running"
 #define PCMK__VALUE_SCHEDULER               "scheduler"
 #define PCMK__VALUE_SHUTDOWN_COMPLETE       "shutdown_complete"
 #define PCMK__VALUE_SHUTTING_DOWN           "shutting_down"
 #define PCMK__VALUE_ST_ASYNC_TIMEOUT_VALUE  "st-async-timeout-value"
 #define PCMK__VALUE_ST_NOTIFY               "st_notify"
 #define PCMK__VALUE_ST_NOTIFY_DISCONNECT    "st_notify_disconnect"
 #define PCMK__VALUE_ST_NOTIFY_FENCE         "st_notify_fence"
 #define PCMK__VALUE_ST_NOTIFY_HISTORY       "st_notify_history"
 #define PCMK__VALUE_ST_NOTIFY_HISTORY_SYNCED    "st_notify_history_synced"
 #define PCMK__VALUE_STARTING_DAEMONS        "starting_daemons"
 #define PCMK__VALUE_STONITH_NG              "stonith-ng"
 #define PCMK__VALUE_WAIT_FOR_PING           "wait_for_ping"
 #define PCMK__VALUE_WARNING                 "warning"
 
 /* @COMPAT Deprecated since 2.1.7 (used with PCMK__XA_ORDERING attribute of
  * resource sets)
  */
 #define PCMK__VALUE_GROUP                   "group"
 
 // @COMPAT Drop when daemon metadata commands are dropped
 #define PCMK__VALUE_TIME                    "time"
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__OPTIONS_INTERNAL__H
diff --git a/include/crm/common/strings.h b/include/crm/common/strings.h
new file mode 100644
index 0000000000..a82ce033f4
--- /dev/null
+++ b/include/crm/common/strings.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2004-2024 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.
+ */
+
+#ifndef PCMK__CRM_COMMON_STRINGS__H
+#define PCMK__CRM_COMMON_STRINGS__H
+
+#include <glib.h>                    // gboolean, guint, G_GNUC_PRINTF
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*!
+ * \file
+ * \brief API for strings
+ * \ingroup core
+ */
+
+// NOTE: sbd (as of at least 1.5.2) uses this
+long long crm_get_msec(const char *input);
+
+int pcmk_parse_interval_spec(const char *input, guint *result_ms);
+
+// NOTE: sbd (as of at least 1.5.2) uses this
+gboolean crm_is_true(const char *s);
+
+int crm_str_to_boolean(const char *s, int *ret);
+
+// NOTE: sbd (as of at least 1.5.2) uses this
+char *crm_strdup_printf(char const *format, ...) G_GNUC_PRINTF(1, 2);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // PCMK__CRM_COMMON_STRINGS__H
diff --git a/include/crm/common/strings_internal.h b/include/crm/common/strings_internal.h
index 6a9b4f1ad5..5d4c0de605 100644
--- a/include/crm/common/strings_internal.h
+++ b/include/crm/common/strings_internal.h
@@ -1,244 +1,244 @@
 /*
  * Copyright 2015-2024 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.
  */
 
 #ifndef PCMK__CRM_COMMON_STRINGS_INTERNAL__H
 #define PCMK__CRM_COMMON_STRINGS_INTERNAL__H
 
 #include <stdbool.h>            // bool
 #include <stdint.h>             // uint32_t, etc.
 
 #include <glib.h>               // guint, GList, GHashTable
 
 #include <crm/common/options.h> // PCMK_VALUE_TRUE, PCMK_VALUE_FALSE
-#include <crm/common/util.h>    // crm_strdup_printf
+#include <crm/common/strings.h> // crm_strdup_printf()
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /* internal constants for generic string functions (from strings.c) */
 
 #define PCMK__PARSE_INT_DEFAULT -1
 #define PCMK__PARSE_DBL_DEFAULT -1.0
 
 /* internal generic string functions (from strings.c) */
 
 enum pcmk__str_flags {
     pcmk__str_none          = 0,
     pcmk__str_casei         = 1 << 0,
     pcmk__str_null_matches  = 1 << 1,
     pcmk__str_regex         = 1 << 2,
     pcmk__str_star_matches  = 1 << 3,
 };
 
 int pcmk__scan_double(const char *text, double *result,
                       const char *default_text, char **end_text);
 int pcmk__guint_from_hash(GHashTable *table, const char *key, guint default_val,
                           guint *result);
 bool pcmk__starts_with(const char *str, const char *prefix);
 bool pcmk__ends_with(const char *s, const char *match);
 bool pcmk__ends_with_ext(const char *s, const char *match);
 char *pcmk__trim(char *str);
 void pcmk__add_separated_word(GString **list, size_t init_size,
                               const char *word, const char *separator);
 int pcmk__compress(const char *data, unsigned int length, unsigned int max,
                    char **result, unsigned int *result_len);
 
 int pcmk__scan_ll(const char *text, long long *result, long long default_value);
 int pcmk__scan_min_int(const char *text, int *result, int minimum);
 int pcmk__scan_port(const char *text, int *port);
 int pcmk__parse_ll_range(const char *srcstring, long long *start, long long *end);
 
 GHashTable *pcmk__strkey_table(GDestroyNotify key_destroy_func,
                                GDestroyNotify value_destroy_func);
 GHashTable *pcmk__strikey_table(GDestroyNotify key_destroy_func,
                                 GDestroyNotify value_destroy_func);
 GHashTable *pcmk__str_table_dup(GHashTable *old_table);
 void pcmk__insert_dup(GHashTable *table, const char *name, const char *value);
 
 /*!
  * \internal
  * \brief Get a string value with a default if NULL
  *
  * \param[in] s              String to return if non-NULL
  * \param[in] default_value  String (or NULL) to return if \p s is NULL
  *
  * \return \p s if \p s is non-NULL, otherwise \p default_value
  */
 static inline const char *
 pcmk__s(const char *s, const char *default_value)
 {
     return (s == NULL)? default_value : s;
 }
 
 /*!
  * \internal
  * \brief Create a hash table with integer keys
  *
  * \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().
  */
 static inline GHashTable *
 pcmk__intkey_table(GDestroyNotify value_destroy_func)
 {
     return g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL,
                                  value_destroy_func);
 }
 
 /*!
  * \internal
  * \brief Insert a value into a hash table with integer keys
  *
  * \param[in,out] hash_table  Table to insert into
  * \param[in]     key         Integer key to insert
  * \param[in]     value       Value to insert
  *
  * \return Whether the key/value was already in the table
  * \note This has the same semantics as g_hash_table_insert(). If the key
  *       already exists in the table, the old value is freed and replaced.
  */
 static inline gboolean
 pcmk__intkey_table_insert(GHashTable *hash_table, int key, gpointer value)
 {
     return g_hash_table_insert(hash_table, GINT_TO_POINTER(key), value);
 }
 
 /*!
  * \internal
  * \brief Look up a value in a hash table with integer keys
  *
  * \param[in] hash_table  Table to check
  * \param[in] key         Integer key to look for
  *
  * \return Value in table for \key (or NULL if not found)
  */
 static inline gpointer
 pcmk__intkey_table_lookup(GHashTable *hash_table, int key)
 {
     return g_hash_table_lookup(hash_table, GINT_TO_POINTER(key));
 }
 
 /*!
  * \internal
  * \brief Remove a key/value from a hash table with integer keys
  *
  * \param[in,out] hash_table  Table to modify
  * \param[in]     key         Integer key of entry to remove
  *
  * \return Whether \p key was found and removed from \p hash_table
  */
 static inline gboolean
 pcmk__intkey_table_remove(GHashTable *hash_table, int key)
 {
     return g_hash_table_remove(hash_table, GINT_TO_POINTER(key));
 }
 
 gboolean pcmk__str_in_list(const gchar *s, const GList *lst, uint32_t flags);
 
 bool pcmk__strcase_any_of(const char *s, ...) G_GNUC_NULL_TERMINATED;
 bool pcmk__str_any_of(const char *s, ...) G_GNUC_NULL_TERMINATED;
 bool pcmk__char_in_any_str(int ch, ...) G_GNUC_NULL_TERMINATED;
 
 int pcmk__strcmp(const char *s1, const char *s2, uint32_t flags);
 int pcmk__numeric_strcasecmp(const char *s1, const char *s2);
 
 char *pcmk__str_copy_as(const char *file, const char *function, uint32_t line,
                         const char *str);
 
 /*!
  * \internal
  * \brief Copy a string, asserting on failure
  *
  * \param[in] str  String to copy (can be \c NULL)
  *
  * \return Newly allocated copy of \p str, or \c NULL if \p str is \c NULL
  *
  * \note The caller is responsible for freeing the return value using \c free().
  */
 #define pcmk__str_copy(str) pcmk__str_copy_as(__FILE__, __func__, __LINE__, str)
 
 void pcmk__str_update(char **str, const char *value);
 
 void pcmk__g_strcat(GString *buffer, ...) G_GNUC_NULL_TERMINATED;
 
 static inline bool
 pcmk__str_eq(const char *s1, const char *s2, uint32_t flags)
 {
     return pcmk__strcmp(s1, s2, flags) == 0;
 }
 
 // Like pcmk__add_separated_word() but using a space as separator
 static inline void
 pcmk__add_word(GString **list, size_t init_size, const char *word)
 {
     return pcmk__add_separated_word(list, init_size, word, " ");
 }
 
 /* Correctly displaying singular or plural is complicated; consider "1 node has"
  * vs. "2 nodes have". A flexible solution is to pluralize entire strings, e.g.
  *
  * if (a == 1) {
  *     crm_info("singular message"):
  * } else {
  *     crm_info("plural message");
  * }
  *
  * though even that's not sufficient for all languages besides English (if we
  * ever desire to do translations of output and log messages). But the following
  * convenience macros are "good enough" and more concise for many cases.
  */
 
 /* Example:
  * crm_info("Found %d %s", nentries,
  *          pcmk__plural_alt(nentries, "entry", "entries"));
  */
 #define pcmk__plural_alt(i, s1, s2) (((i) == 1)? (s1) : (s2))
 
 // Example: crm_info("Found %d node%s", nnodes, pcmk__plural_s(nnodes));
 #define pcmk__plural_s(i) pcmk__plural_alt(i, "", "s")
 
 static inline int
 pcmk__str_empty(const char *s)
 {
     return (s == NULL) || (s[0] == '\0');
 }
 
 static inline char *
 pcmk__itoa(int an_int)
 {
     return crm_strdup_printf("%d", an_int);
 }
 
 static inline char *
 pcmk__ftoa(double a_float)
 {
     return crm_strdup_printf("%f", a_float);
 }
 
 static inline char *
 pcmk__ttoa(time_t epoch_time)
 {
     return crm_strdup_printf("%lld", (long long) epoch_time);
 }
 
 // note this returns const not allocated
 static inline const char *
 pcmk__btoa(bool condition)
 {
     return condition? PCMK_VALUE_TRUE : PCMK_VALUE_FALSE;
 }
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_STRINGS_INTERNAL__H
diff --git a/include/crm/common/util.h b/include/crm/common/util.h
index b88c139c39..bfb0d43a21 100644
--- a/include/crm/common/util.h
+++ b/include/crm/common/util.h
@@ -1,122 +1,106 @@
 /*
  * Copyright 2004-2024 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.
  */
 
 #ifndef PCMK__CRM_COMMON_UTIL__H
 #define PCMK__CRM_COMMON_UTIL__H
 
 #include <sys/types.h>    // gid_t, mode_t, size_t, time_t, uid_t
 #include <stdlib.h>
 #include <stdbool.h>
 #include <stdint.h>       // uint32_t
 #include <limits.h>
 #include <signal.h>
 #include <glib.h>
 
 #include <crm/common/acl.h>
 #include <crm/common/actions.h>
 #include <crm/common/agents.h>
 #include <crm/common/results.h>
 #include <crm/common/scores.h>
+#include <crm/common/strings.h>
 #include <crm/common/nvpair.h>
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Utility functions
  * \ingroup core
  */
 
 /* public node attribute functions (from attrd_client.c) */
 char *pcmk_promotion_score_name(const char *rsc_id);
 
 /* public Pacemaker Remote functions (from remote.c) */
 int crm_default_remote_port(void);
 
-/* public string functions (from strings.c) */
-
-// NOTE: sbd (as of at least 1.5.2) uses this
-gboolean crm_is_true(const char *s);
-
-int crm_str_to_boolean(const char *s, int *ret);
-
-// NOTE: sbd (as of at least 1.5.2) uses this
-long long crm_get_msec(const char *input);
-
-char * crm_strip_trailing_newline(char *str);
-
-// NOTE: sbd (as of at least 1.5.2) uses this
-char *crm_strdup_printf(char const *format, ...) G_GNUC_PRINTF(1, 2);
-
-int pcmk_parse_interval_spec(const char *input, guint *result_ms);
-
 int compare_version(const char *version1, const char *version2);
 
 /* coverity[+kill] */
 void crm_abort(const char *file, const char *function, int line,
                const char *condition, gboolean do_core, gboolean do_fork);
 
 /*!
  * \brief Check whether any of specified flags are set in a flag group
  *
  * \param[in] flag_group        The flag group being examined
  * \param[in] flags_to_check    Which flags in flag_group should be checked
  *
  * \return true if \p flags_to_check is nonzero and any of its flags are set in
  *         \p flag_group, or false otherwise
  */
 static inline bool
 pcmk_any_flags_set(uint64_t flag_group, uint64_t flags_to_check)
 {
     return (flag_group & flags_to_check) != 0;
 }
 
 /*!
  * \brief Check whether all of specified flags are set in a flag group
  *
  * \param[in] flag_group        The flag group being examined
  * \param[in] flags_to_check    Which flags in flag_group should be checked
  *
  * \return true if \p flags_to_check is zero or all of its flags are set in
  *         \p flag_group, or false otherwise
  */
 static inline bool
 pcmk_all_flags_set(uint64_t flag_group, uint64_t flags_to_check)
 {
     return (flag_group & flags_to_check) == flags_to_check;
 }
 
 /*!
  * \brief Convenience alias for pcmk_all_flags_set(), to check single flag
  */
 #define pcmk_is_set(g, f)   pcmk_all_flags_set((g), (f))
 
 char *crm_md5sum(const char *buffer);
 
 char *crm_generate_uuid(void);
 
 int crm_user_lookup(const char *name, uid_t * uid, gid_t * gid);
 int pcmk_daemon_user(uid_t *uid, gid_t *gid);
 
 void crm_gnutls_global_init(void);
 
 bool pcmk_str_is_infinity(const char *s);
 bool pcmk_str_is_minus_infinity(const char *s);
 
 #ifdef __cplusplus
 }
 #endif
 
 #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
 #include <crm/common/util_compat.h>
 #endif
 
 #endif