Page MenuHomeClusterLabs Projects

No OneTemporary

diff --git a/lib/cib/cib_file.c b/lib/cib/cib_file.c
index 24bd029406..9bb076743e 100644
--- a/lib/cib/cib_file.c
+++ b/lib/cib/cib_file.c
@@ -1,1176 +1,1188 @@
/*
* Original copyright 2004 International Business Machines
* Later changes copyright 2008-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.
*/
#include <crm_internal.h>
#include <unistd.h>
#include <limits.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <pwd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <glib.h>
#include <crm/crm.h>
#include <crm/cib/internal.h>
#include <crm/common/ipc.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#define CIB_SERIES "cib"
#define CIB_SERIES_MAX 100
#define CIB_SERIES_BZIP FALSE /* Must be false because archived copies are
created with hard links
*/
#define CIB_LIVE_NAME CIB_SERIES ".xml"
// key: client ID (const char *) -> value: client (cib_t *)
static GHashTable *client_table = NULL;
enum cib_file_flags {
cib_file_flag_dirty = (1 << 0),
cib_file_flag_live = (1 << 1),
};
typedef struct cib_file_opaque_s {
char *id;
char *filename;
uint32_t flags; // Group of enum cib_file_flags
xmlNode *cib_xml;
} cib_file_opaque_t;
static int cib_file_process_commit_transaction(const char *op, int options,
const char *section,
xmlNode *req, xmlNode *input,
xmlNode *existing_cib,
xmlNode **result_cib,
xmlNode **answer);
/*!
* \internal
* \brief Add a CIB file client to client table
*
* \param[in] cib CIB client
*/
static void
register_client(const cib_t *cib)
{
cib_file_opaque_t *private = cib->variant_opaque;
if (client_table == NULL) {
client_table = pcmk__strkey_table(NULL, NULL);
}
g_hash_table_insert(client_table, private->id, (gpointer) cib);
}
/*!
* \internal
* \brief Remove a CIB file client from client table
*
* \param[in] cib CIB client
*/
static void
unregister_client(const cib_t *cib)
{
cib_file_opaque_t *private = cib->variant_opaque;
if (client_table == NULL) {
return;
}
g_hash_table_remove(client_table, private->id);
/* @COMPAT: Add to crm_exit() when libcib and libcrmcommon are merged,
* instead of destroying the client table when there are no more clients.
*/
if (g_hash_table_size(client_table) == 0) {
g_hash_table_destroy(client_table);
client_table = NULL;
}
}
/*!
* \internal
* \brief Look up a CIB file client by its ID
*
* \param[in] client_id CIB client ID
*
* \return CIB client with matching ID if found, or \p NULL otherwise
*/
static cib_t *
get_client(const char *client_id)
{
if (client_table == NULL) {
return NULL;
}
return g_hash_table_lookup(client_table, (gpointer) client_id);
}
static const cib__op_fn_t cib_op_functions[] = {
[cib__op_apply_patch] = cib_process_diff,
[cib__op_bump] = cib_process_bump,
[cib__op_commit_transact] = cib_file_process_commit_transaction,
[cib__op_create] = cib_process_create,
[cib__op_delete] = cib_process_delete,
[cib__op_erase] = cib_process_erase,
[cib__op_modify] = cib_process_modify,
[cib__op_query] = cib_process_query,
[cib__op_replace] = cib_process_replace,
[cib__op_upgrade] = cib_process_upgrade,
};
/* cib_file_backup() and cib_file_write_with_digest() need to chown the
* written files only in limited circumstances, so these variables allow
* that to be indicated without affecting external callers
*/
static uid_t cib_file_owner = 0;
static uid_t cib_file_group = 0;
static gboolean cib_do_chown = FALSE;
#define cib_set_file_flags(cibfile, flags_to_set) do { \
(cibfile)->flags = pcmk__set_flags_as(__func__, __LINE__, \
LOG_TRACE, "CIB file", \
cibfile->filename, \
(cibfile)->flags, \
(flags_to_set), \
#flags_to_set); \
} while (0)
#define cib_clear_file_flags(cibfile, flags_to_clear) do { \
(cibfile)->flags = pcmk__clear_flags_as(__func__, __LINE__, \
LOG_TRACE, "CIB file", \
cibfile->filename, \
(cibfile)->flags, \
(flags_to_clear), \
#flags_to_clear); \
} while (0)
/*!
* \internal
* \brief Get the function that performs a given CIB file operation
*
* \param[in] operation Operation whose function to look up
*
* \return Function that performs \p operation for a CIB file client
*/
static cib__op_fn_t
file_get_op_function(const cib__operation_t *operation)
{
enum cib__op_type type = operation->type;
CRM_ASSERT(type >= 0);
if (type >= PCMK__NELEM(cib_op_functions)) {
return NULL;
}
return cib_op_functions[type];
}
/*!
* \internal
* \brief Check whether a file is the live CIB
*
* \param[in] filename Name of file to check
*
* \return TRUE if file exists and its real path is same as live CIB's
*/
static gboolean
cib_file_is_live(const char *filename)
{
gboolean same = FALSE;
if (filename != NULL) {
// Canonicalize file names for true comparison
char *real_filename = NULL;
if (pcmk__real_path(filename, &real_filename) == pcmk_rc_ok) {
char *real_livename = NULL;
if (pcmk__real_path(CRM_CONFIG_DIR "/" CIB_LIVE_NAME,
&real_livename) == pcmk_rc_ok) {
same = !strcmp(real_filename, real_livename);
free(real_livename);
}
free(real_filename);
}
}
return same;
}
static int
cib_file_process_request(cib_t *cib, xmlNode *request, xmlNode **output)
{
int rc = pcmk_ok;
const cib__operation_t *operation = NULL;
cib__op_fn_t op_function = NULL;
int call_id = 0;
int call_options = cib_none;
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
const char *section = crm_element_value(request, PCMK__XA_CIB_SECTION);
xmlNode *wrapper = pcmk__xe_first_child(request, PCMK__XE_CIB_CALLDATA,
NULL, NULL);
xmlNode *data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
bool changed = false;
bool read_only = false;
xmlNode *result_cib = NULL;
xmlNode *cib_diff = NULL;
cib_file_opaque_t *private = cib->variant_opaque;
// We error checked these in callers
cib__get_operation(op, &operation);
op_function = file_get_op_function(operation);
crm_element_value_int(request, PCMK__XA_CIB_CALLID, &call_id);
crm_element_value_int(request, PCMK__XA_CIB_CALLOPT, &call_options);
read_only = !pcmk_is_set(operation->flags, cib__op_attr_modifies);
// Mirror the logic in prepare_input() in pacemaker-based
if ((section != NULL) && pcmk__xe_is(data, PCMK_XE_CIB)) {
data = pcmk_find_cib_element(data, section);
}
rc = cib_perform_op(cib, op, call_options, op_function, read_only, section,
request, data, true, &changed, &private->cib_xml,
&result_cib, &cib_diff, output);
if (pcmk_is_set(call_options, cib_transaction)) {
/* The rest of the logic applies only to the transaction as a whole, not
* to individual requests.
*/
goto done;
}
if (rc == -pcmk_err_schema_validation) {
// Show validation errors to stderr
pcmk__validate_xml(result_cib, NULL, NULL, NULL);
} else if ((rc == pcmk_ok) && !read_only) {
pcmk__log_xml_patchset(LOG_DEBUG, cib_diff);
if (result_cib != private->cib_xml) {
free_xml(private->cib_xml);
private->cib_xml = result_cib;
}
cib_set_file_flags(private, cib_file_flag_dirty);
}
// Global operation callback (deprecated)
if (cib->op_callback != NULL) {
cib->op_callback(NULL, call_id, rc, *output);
}
done:
if ((result_cib != private->cib_xml) && (result_cib != *output)) {
free_xml(result_cib);
}
free_xml(cib_diff);
return rc;
}
static int
cib_file_perform_op_delegate(cib_t *cib, const char *op, const char *host,
const char *section, xmlNode *data,
xmlNode **output_data, int call_options,
const char *user_name)
{
int rc = pcmk_ok;
xmlNode *request = NULL;
xmlNode *output = NULL;
cib_file_opaque_t *private = cib->variant_opaque;
const cib__operation_t *operation = NULL;
crm_info("Handling %s operation for %s as %s",
pcmk__s(op, "invalid"), pcmk__s(section, "entire CIB"),
pcmk__s(user_name, "default user"));
if (output_data != NULL) {
*output_data = NULL;
}
if (cib->state == cib_disconnected) {
return -ENOTCONN;
}
rc = cib__get_operation(op, &operation);
rc = pcmk_rc2legacy(rc);
if (rc != pcmk_ok) {
// @COMPAT: At compatibility break, use rc directly
return -EPROTONOSUPPORT;
}
if (file_get_op_function(operation) == NULL) {
// @COMPAT: At compatibility break, use EOPNOTSUPP
crm_err("Operation %s is not supported by CIB file clients", op);
return -EPROTONOSUPPORT;
}
cib__set_call_options(call_options, "file operation", cib_no_mtime);
rc = cib__create_op(cib, op, host, section, data, call_options, user_name,
NULL, &request);
if (rc != pcmk_ok) {
return rc;
}
crm_xml_add(request, PCMK_XE_ACL_TARGET, user_name);
crm_xml_add(request, PCMK__XA_CIB_CLIENTID, private->id);
if (pcmk_is_set(call_options, cib_transaction)) {
rc = cib__extend_transaction(cib, request);
goto done;
}
rc = cib_file_process_request(cib, request, &output);
if ((output_data != NULL) && (output != NULL)) {
if (output->doc == private->cib_xml->doc) {
*output_data = pcmk__xml_copy(NULL, output);
} else {
*output_data = output;
}
}
done:
if ((output != NULL)
&& (output->doc != private->cib_xml->doc)
&& ((output_data == NULL) || (output != *output_data))) {
free_xml(output);
}
free_xml(request);
return rc;
}
/*!
* \internal
* \brief Read CIB from disk and validate it against XML schema
*
* \param[in] filename Name of file to read CIB from
* \param[out] output Where to store the read CIB XML
*
* \return pcmk_ok on success,
* -ENXIO if file does not exist (or stat() otherwise fails), or
* -pcmk_err_schema_validation if XML doesn't parse or validate
* \note If filename is the live CIB, this will *not* verify its digest,
* though that functionality would be trivial to add here.
* Also, this will *not* verify that the file is writable,
* because some callers might not need to write.
*/
static int
load_file_cib(const char *filename, xmlNode **output)
{
struct stat buf;
xmlNode *root = NULL;
/* Ensure file is readable */
if (strcmp(filename, "-") && (stat(filename, &buf) < 0)) {
return -ENXIO;
}
/* Parse XML from file */
root = pcmk__xml_read(filename);
if (root == NULL) {
return -pcmk_err_schema_validation;
}
/* Add a status section if not already present */
if (pcmk__xe_first_child(root, PCMK_XE_STATUS, NULL, NULL) == NULL) {
pcmk__xe_create(root, PCMK_XE_STATUS);
}
/* Validate XML against its specified schema */
if (!pcmk__configured_schema_validates(root)) {
const char *schema = crm_element_value(root, PCMK_XA_VALIDATE_WITH);
crm_err("CIB does not validate against %s, or that schema is unknown", schema);
free_xml(root);
return -pcmk_err_schema_validation;
}
/* Remember the parsed XML for later use */
*output = root;
return pcmk_ok;
}
static int
cib_file_signon(cib_t *cib, const char *name, enum cib_conn_type type)
{
int rc = pcmk_ok;
cib_file_opaque_t *private = cib->variant_opaque;
if (private->filename == NULL) {
rc = -EINVAL;
} else {
rc = load_file_cib(private->filename, &private->cib_xml);
}
if (rc == pcmk_ok) {
crm_debug("Opened connection to local file '%s' for %s",
private->filename, name);
cib->state = cib_connected_command;
cib->type = cib_command;
register_client(cib);
} else {
crm_info("Connection to local file '%s' for %s (client %s) failed: %s",
private->filename, name, private->id, pcmk_strerror(rc));
}
return rc;
}
/*!
* \internal
* \brief Write out the in-memory CIB to a live CIB file
*
* \param[in] cib_root Root of XML tree to write
* \param[in,out] path Full path to file to write
*
* \return 0 on success, -1 on failure
*/
static int
cib_file_write_live(xmlNode *cib_root, char *path)
{
uid_t uid = geteuid();
struct passwd *daemon_pwent;
char *sep = strrchr(path, '/');
const char *cib_dirname, *cib_filename;
int rc = 0;
/* Get the desired uid/gid */
errno = 0;
daemon_pwent = getpwnam(CRM_DAEMON_USER);
if (daemon_pwent == NULL) {
crm_perror(LOG_ERR, "Could not find %s user", CRM_DAEMON_USER);
return -1;
}
/* If we're root, we can change the ownership;
* if we're daemon, anything we create will be OK;
* otherwise, block access so we don't create wrong owner
*/
if ((uid != 0) && (uid != daemon_pwent->pw_uid)) {
crm_perror(LOG_ERR, "Must be root or %s to modify live CIB",
CRM_DAEMON_USER);
return 0;
}
/* fancy footwork to separate dirname from filename
* (we know the canonical name maps to the live CIB,
* but the given name might be relative, or symlinked)
*/
if (sep == NULL) { /* no directory component specified */
cib_dirname = "./";
cib_filename = path;
} else if (sep == path) { /* given name is in / */
cib_dirname = "/";
cib_filename = path + 1;
} else { /* typical case; split given name into parts */
*sep = '\0';
cib_dirname = path;
cib_filename = sep + 1;
}
/* if we're root, we want to update the file ownership */
if (uid == 0) {
cib_file_owner = daemon_pwent->pw_uid;
cib_file_group = daemon_pwent->pw_gid;
cib_do_chown = TRUE;
}
/* write the file */
if (cib_file_write_with_digest(cib_root, cib_dirname,
cib_filename) != pcmk_ok) {
rc = -1;
}
/* turn off file ownership changes, for other callers */
if (uid == 0) {
cib_do_chown = FALSE;
}
/* undo fancy stuff */
if ((sep != NULL) && (*sep == '\0')) {
*sep = '/';
}
return rc;
}
/*!
* \internal
* \brief Sign-off method for CIB file variants
*
* This will write the file to disk if needed, and free the in-memory CIB. If
* the file is the live CIB, it will compute and write a signature as well.
*
* \param[in,out] cib CIB object to sign off
*
* \return pcmk_ok on success, pcmk_err_generic on failure
* \todo This method should refuse to write the live CIB if the CIB manager is
* running.
*/
static int
cib_file_signoff(cib_t *cib)
{
int rc = pcmk_ok;
cib_file_opaque_t *private = cib->variant_opaque;
crm_debug("Disconnecting from the CIB manager");
cib->state = cib_disconnected;
cib->type = cib_no_connection;
unregister_client(cib);
cib->cmds->end_transaction(cib, false, cib_none);
/* If the in-memory CIB has been changed, write it to disk */
if (pcmk_is_set(private->flags, cib_file_flag_dirty)) {
/* If this is the live CIB, write it out with a digest */
if (pcmk_is_set(private->flags, cib_file_flag_live)) {
if (cib_file_write_live(private->cib_xml, private->filename) < 0) {
rc = pcmk_err_generic;
}
/* Otherwise, it's a simple write */
} else {
bool compress = pcmk__ends_with_ext(private->filename, ".bz2");
if (pcmk__xml_write_file(private->cib_xml, private->filename,
compress, NULL) != pcmk_rc_ok) {
rc = pcmk_err_generic;
}
}
if (rc == pcmk_ok) {
crm_info("Wrote CIB to %s", private->filename);
cib_clear_file_flags(private, cib_file_flag_dirty);
} else {
crm_err("Could not write CIB to %s", private->filename);
}
}
/* Free the in-memory CIB */
free_xml(private->cib_xml);
private->cib_xml = NULL;
return rc;
}
static int
cib_file_free(cib_t *cib)
{
int rc = pcmk_ok;
if (cib->state != cib_disconnected) {
rc = cib_file_signoff(cib);
}
if (rc == pcmk_ok) {
cib_file_opaque_t *private = cib->variant_opaque;
free(private->id);
free(private->filename);
free(private);
free(cib->cmds);
free(cib->user);
free(cib);
} else {
fprintf(stderr, "Couldn't sign off: %d\n", rc);
}
return rc;
}
static int
cib_file_inputfd(cib_t *cib)
{
return -EPROTONOSUPPORT;
}
static int
cib_file_register_notification(cib_t *cib, const char *callback, int enabled)
{
return -EPROTONOSUPPORT;
}
static int
cib_file_set_connection_dnotify(cib_t *cib,
void (*dnotify) (gpointer user_data))
{
return -EPROTONOSUPPORT;
}
/*!
* \internal
* \brief Get the given CIB connection's unique client identifier
*
* \param[in] cib CIB connection
* \param[out] async_id If not \p NULL, where to store asynchronous client ID
* \param[out] sync_id If not \p NULL, where to store synchronous client ID
*
* \return Legacy Pacemaker return code
*
* \note This is the \p cib_file variant implementation of
* \p cib_api_operations_t:client_id().
*/
static int
cib_file_client_id(const cib_t *cib, const char **async_id,
const char **sync_id)
{
cib_file_opaque_t *private = cib->variant_opaque;
if (async_id != NULL) {
*async_id = private->id;
}
if (sync_id != NULL) {
*sync_id = private->id;
}
return pcmk_ok;
}
cib_t *
cib_file_new(const char *cib_location)
{
+ cib_t *cib = NULL;
cib_file_opaque_t *private = NULL;
- cib_t *cib = cib_new_variant();
+ char *filename = NULL;
+
+ if (cib_location == NULL) {
+ cib_location = getenv("CIB_file");
+ if (cib_location == NULL) {
+ return NULL; // Shouldn't be possible if we were called internally
+ }
+ }
+ cib = cib_new_variant();
if (cib == NULL) {
return NULL;
}
- private = calloc(1, sizeof(cib_file_opaque_t));
+ filename = strdup(cib_location);
+ if (filename == NULL) {
+ free(cib);
+ return NULL;
+ }
+ private = calloc(1, sizeof(cib_file_opaque_t));
if (private == NULL) {
free(cib);
+ free(filename);
return NULL;
}
+
private->id = crm_generate_uuid();
+ private->filename = filename;
cib->variant = cib_file;
cib->variant_opaque = private;
- if (cib_location == NULL) {
- cib_location = getenv("CIB_file");
- CRM_CHECK(cib_location != NULL, return NULL); // Shouldn't be possible
- }
private->flags = 0;
if (cib_file_is_live(cib_location)) {
cib_set_file_flags(private, cib_file_flag_live);
crm_trace("File %s detected as live CIB", cib_location);
}
- private->filename = strdup(cib_location);
/* assign variant specific ops */
cib->delegate_fn = cib_file_perform_op_delegate;
cib->cmds->signon = cib_file_signon;
cib->cmds->signoff = cib_file_signoff;
cib->cmds->free = cib_file_free;
cib->cmds->inputfd = cib_file_inputfd; // Deprecated method
cib->cmds->register_notification = cib_file_register_notification;
cib->cmds->set_connection_dnotify = cib_file_set_connection_dnotify;
cib->cmds->client_id = cib_file_client_id;
return cib;
}
/*!
* \internal
* \brief Compare the calculated digest of an XML tree against a signature file
*
* \param[in] root Root of XML tree to compare
* \param[in] sigfile Name of signature file containing digest to compare
*
* \return TRUE if digests match or signature file does not exist, else FALSE
*/
static gboolean
cib_file_verify_digest(xmlNode *root, const char *sigfile)
{
gboolean passed = FALSE;
char *expected;
int rc = pcmk__file_contents(sigfile, &expected);
switch (rc) {
case pcmk_rc_ok:
if (expected == NULL) {
crm_err("On-disk digest at %s is empty", sigfile);
return FALSE;
}
break;
case ENOENT:
crm_warn("No on-disk digest present at %s", sigfile);
return TRUE;
default:
crm_err("Could not read on-disk digest from %s: %s",
sigfile, pcmk_rc_str(rc));
return FALSE;
}
passed = pcmk__verify_digest(root, expected);
free(expected);
return passed;
}
/*!
* \internal
* \brief Read an XML tree from a file and verify its digest
*
* \param[in] filename Name of XML file to read
* \param[in] sigfile Name of signature file containing digest to compare
* \param[out] root If non-NULL, will be set to pointer to parsed XML tree
*
* \return 0 if file was successfully read, parsed and verified, otherwise:
* -errno on stat() failure,
* -pcmk_err_cib_corrupt if file size is 0 or XML is not parseable, or
* -pcmk_err_cib_modified if digests do not match
* \note If root is non-NULL, it is the caller's responsibility to free *root on
* successful return.
*/
int
cib_file_read_and_verify(const char *filename, const char *sigfile, xmlNode **root)
{
int s_res;
struct stat buf;
char *local_sigfile = NULL;
xmlNode *local_root = NULL;
CRM_ASSERT(filename != NULL);
if (root) {
*root = NULL;
}
/* Verify that file exists and its size is nonzero */
s_res = stat(filename, &buf);
if (s_res < 0) {
crm_perror(LOG_WARNING, "Could not verify cluster configuration file %s", filename);
return -errno;
} else if (buf.st_size == 0) {
crm_warn("Cluster configuration file %s is corrupt (size is zero)", filename);
return -pcmk_err_cib_corrupt;
}
/* Parse XML */
local_root = pcmk__xml_read(filename);
if (local_root == NULL) {
crm_warn("Cluster configuration file %s is corrupt (unparseable as XML)", filename);
return -pcmk_err_cib_corrupt;
}
/* If sigfile is not specified, use original file name plus .sig */
if (sigfile == NULL) {
sigfile = local_sigfile = crm_strdup_printf("%s.sig", filename);
}
/* Verify that digests match */
if (cib_file_verify_digest(local_root, sigfile) == FALSE) {
free(local_sigfile);
free_xml(local_root);
return -pcmk_err_cib_modified;
}
free(local_sigfile);
if (root) {
*root = local_root;
} else {
free_xml(local_root);
}
return pcmk_ok;
}
/*!
* \internal
* \brief Back up a CIB
*
* \param[in] cib_dirname Directory containing CIB file and backups
* \param[in] cib_filename Name (relative to cib_dirname) of CIB file to back up
*
* \return 0 on success, -1 on error
*/
static int
cib_file_backup(const char *cib_dirname, const char *cib_filename)
{
int rc = 0;
unsigned int seq;
char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename);
char *cib_digest = crm_strdup_printf("%s.sig", cib_path);
char *backup_path;
char *backup_digest;
// Determine backup and digest file names
if (pcmk__read_series_sequence(cib_dirname, CIB_SERIES,
&seq) != pcmk_rc_ok) {
// @TODO maybe handle errors better ...
seq = 0;
}
backup_path = pcmk__series_filename(cib_dirname, CIB_SERIES, seq,
CIB_SERIES_BZIP);
backup_digest = crm_strdup_printf("%s.sig", backup_path);
/* Remove the old backups if they exist */
unlink(backup_path);
unlink(backup_digest);
/* Back up the CIB, by hard-linking it to the backup name */
if ((link(cib_path, backup_path) < 0) && (errno != ENOENT)) {
crm_perror(LOG_ERR, "Could not archive %s by linking to %s",
cib_path, backup_path);
rc = -1;
/* Back up the CIB signature similarly */
} else if ((link(cib_digest, backup_digest) < 0) && (errno != ENOENT)) {
crm_perror(LOG_ERR, "Could not archive %s by linking to %s",
cib_digest, backup_digest);
rc = -1;
/* Update the last counter and ensure everything is sync'd to media */
} else {
pcmk__write_series_sequence(cib_dirname, CIB_SERIES, ++seq,
CIB_SERIES_MAX);
if (cib_do_chown) {
int rc2;
if ((chown(backup_path, cib_file_owner, cib_file_group) < 0)
&& (errno != ENOENT)) {
crm_perror(LOG_ERR, "Could not set owner of %s", backup_path);
rc = -1;
}
if ((chown(backup_digest, cib_file_owner, cib_file_group) < 0)
&& (errno != ENOENT)) {
crm_perror(LOG_ERR, "Could not set owner of %s", backup_digest);
rc = -1;
}
rc2 = pcmk__chown_series_sequence(cib_dirname, CIB_SERIES,
cib_file_owner, cib_file_group);
if (rc2 != pcmk_rc_ok) {
crm_err("Could not set owner of sequence file in %s: %s",
cib_dirname, pcmk_rc_str(rc2));
rc = -1;
}
}
pcmk__sync_directory(cib_dirname);
crm_info("Archived previous version as %s", backup_path);
}
free(cib_path);
free(cib_digest);
free(backup_path);
free(backup_digest);
return rc;
}
/*!
* \internal
* \brief Prepare CIB XML to be written to disk
*
* Set \c PCMK_XA_NUM_UPDATES to 0, set \c PCMK_XA_CIB_LAST_WRITTEN to the
* current timestamp, and strip out the status section.
*
* \param[in,out] root Root of CIB XML tree
*
* \return void
*/
static void
cib_file_prepare_xml(xmlNode *root)
{
xmlNode *cib_status_root = NULL;
/* Always write out with num_updates=0 and current last-written timestamp */
crm_xml_add(root, PCMK_XA_NUM_UPDATES, "0");
pcmk__xe_add_last_written(root);
/* Delete status section before writing to file, because
* we discard it on startup anyway, and users get confused by it */
cib_status_root = pcmk__xe_first_child(root, PCMK_XE_STATUS, NULL, NULL);
CRM_CHECK(cib_status_root != NULL, return);
free_xml(cib_status_root);
}
/*!
* \internal
* \brief Write CIB to disk, along with a signature file containing its digest
*
* \param[in,out] cib_root Root of XML tree to write
* \param[in] cib_dirname Directory containing CIB and signature files
* \param[in] cib_filename Name (relative to cib_dirname) of file to write
*
* \return pcmk_ok on success,
* pcmk_err_cib_modified if existing cib_filename doesn't match digest,
* pcmk_err_cib_backup if existing cib_filename couldn't be backed up,
* or pcmk_err_cib_save if new cib_filename couldn't be saved
*/
int
cib_file_write_with_digest(xmlNode *cib_root, const char *cib_dirname,
const char *cib_filename)
{
int exit_rc = pcmk_ok;
int rc, fd;
char *digest = NULL;
/* Detect CIB version for diagnostic purposes */
const char *epoch = crm_element_value(cib_root, PCMK_XA_EPOCH);
const char *admin_epoch = crm_element_value(cib_root, PCMK_XA_ADMIN_EPOCH);
/* Determine full CIB and signature pathnames */
char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename);
char *digest_path = crm_strdup_printf("%s.sig", cib_path);
/* Create temporary file name patterns for writing out CIB and signature */
char *tmp_cib = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname);
char *tmp_digest = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname);
/* Ensure the admin didn't modify the existing CIB underneath us */
crm_trace("Reading cluster configuration file %s", cib_path);
rc = cib_file_read_and_verify(cib_path, NULL, NULL);
if ((rc != pcmk_ok) && (rc != -ENOENT)) {
crm_err("%s was manually modified while the cluster was active!",
cib_path);
exit_rc = pcmk_err_cib_modified;
goto cleanup;
}
/* Back up the existing CIB */
if (cib_file_backup(cib_dirname, cib_filename) < 0) {
exit_rc = pcmk_err_cib_backup;
goto cleanup;
}
crm_debug("Writing CIB to disk");
umask(S_IWGRP | S_IWOTH | S_IROTH);
cib_file_prepare_xml(cib_root);
/* Write the CIB to a temporary file, so we can deploy (near) atomically */
fd = mkstemp(tmp_cib);
if (fd < 0) {
crm_perror(LOG_ERR, "Couldn't open temporary file %s for writing CIB",
tmp_cib);
exit_rc = pcmk_err_cib_save;
goto cleanup;
}
/* Protect the temporary file */
if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) {
crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB",
tmp_cib);
exit_rc = pcmk_err_cib_save;
goto cleanup;
}
if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) {
crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB",
tmp_cib);
exit_rc = pcmk_err_cib_save;
goto cleanup;
}
/* Write out the CIB */
if (pcmk__xml_write_fd(cib_root, tmp_cib, fd, false, NULL) != pcmk_rc_ok) {
crm_err("Changes couldn't be written to %s", tmp_cib);
exit_rc = pcmk_err_cib_save;
goto cleanup;
}
/* Calculate CIB digest */
digest = calculate_on_disk_digest(cib_root);
CRM_ASSERT(digest != NULL);
crm_info("Wrote version %s.%s.0 of the CIB to disk (digest: %s)",
(admin_epoch ? admin_epoch : "0"), (epoch ? epoch : "0"), digest);
/* Write the CIB digest to a temporary file */
fd = mkstemp(tmp_digest);
if (fd < 0) {
crm_perror(LOG_ERR, "Could not create temporary file for CIB digest");
exit_rc = pcmk_err_cib_save;
goto cleanup;
}
if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) {
crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB",
tmp_cib);
exit_rc = pcmk_err_cib_save;
close(fd);
goto cleanup;
}
rc = pcmk__write_sync(fd, digest);
if (rc != pcmk_rc_ok) {
crm_err("Could not write digest to %s: %s",
tmp_digest, pcmk_rc_str(rc));
exit_rc = pcmk_err_cib_save;
close(fd);
goto cleanup;
}
close(fd);
crm_debug("Wrote digest %s to disk", digest);
/* Verify that what we wrote is sane */
crm_info("Reading cluster configuration file %s (digest: %s)",
tmp_cib, tmp_digest);
rc = cib_file_read_and_verify(tmp_cib, tmp_digest, NULL);
CRM_ASSERT(rc == 0);
/* Rename temporary files to live, and sync directory changes to media */
crm_debug("Activating %s", tmp_cib);
if (rename(tmp_cib, cib_path) < 0) {
crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_cib, cib_path);
exit_rc = pcmk_err_cib_save;
}
if (rename(tmp_digest, digest_path) < 0) {
crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_digest,
digest_path);
exit_rc = pcmk_err_cib_save;
}
pcmk__sync_directory(cib_dirname);
cleanup:
free(cib_path);
free(digest_path);
free(digest);
free(tmp_digest);
free(tmp_cib);
return exit_rc;
}
/*!
* \internal
* \brief Process requests in a CIB transaction
*
* Stop when a request fails or when all requests have been processed.
*
* \param[in,out] cib CIB client
* \param[in,out] transaction CIB transaction
*
* \return Standard Pacemaker return code
*/
static int
cib_file_process_transaction_requests(cib_t *cib, xmlNode *transaction)
{
cib_file_opaque_t *private = cib->variant_opaque;
for (xmlNode *request = pcmk__xe_first_child(transaction,
PCMK__XE_CIB_COMMAND, NULL,
NULL);
request != NULL; request = pcmk__xe_next_same(request)) {
xmlNode *output = NULL;
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
int rc = cib_file_process_request(cib, request, &output);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
crm_err("Aborting transaction for CIB file client (%s) on file "
"'%s' due to failed %s request: %s",
private->id, private->filename, op, pcmk_rc_str(rc));
crm_log_xml_info(request, "Failed request");
return rc;
}
crm_trace("Applied %s request to transaction working CIB for CIB file "
"client (%s) on file '%s'",
op, private->id, private->filename);
crm_log_xml_trace(request, "Successful request");
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Commit a given CIB file client's transaction to a working CIB copy
*
* \param[in,out] cib CIB file client
* \param[in] transaction CIB transaction
* \param[in,out] result_cib Where to store result CIB
*
* \return Standard Pacemaker return code
*
* \note The caller is responsible for replacing the \p cib argument's
* \p private->cib_xml with \p result_cib on success, and for freeing
* \p result_cib using \p free_xml() on failure.
*/
static int
cib_file_commit_transaction(cib_t *cib, xmlNode *transaction,
xmlNode **result_cib)
{
int rc = pcmk_rc_ok;
cib_file_opaque_t *private = cib->variant_opaque;
xmlNode *saved_cib = private->cib_xml;
CRM_CHECK(pcmk__xe_is(transaction, PCMK__XE_CIB_TRANSACTION),
return pcmk_rc_no_transaction);
/* *result_cib should be a copy of private->cib_xml (created by
* cib_perform_op()). If not, make a copy now. Change tracking isn't
* strictly required here because:
* * Each request in the transaction will have changes tracked and ACLs
* checked if appropriate.
* * cib_perform_op() will infer changes for the commit request at the end.
*/
CRM_CHECK((*result_cib != NULL) && (*result_cib != private->cib_xml),
*result_cib = pcmk__xml_copy(NULL, private->cib_xml));
crm_trace("Committing transaction for CIB file client (%s) on file '%s' to "
"working CIB",
private->id, private->filename);
// Apply all changes to a working copy of the CIB
private->cib_xml = *result_cib;
rc = cib_file_process_transaction_requests(cib, transaction);
crm_trace("Transaction commit %s for CIB file client (%s) on file '%s'",
((rc == pcmk_rc_ok)? "succeeded" : "failed"),
private->id, private->filename);
/* Some request types (for example, erase) may have freed private->cib_xml
* (the working copy) and pointed it at a new XML object. In that case, it
* follows that *result_cib (the working copy) was freed.
*
* Point *result_cib at the updated working copy stored in private->cib_xml.
*/
*result_cib = private->cib_xml;
// Point private->cib_xml back to the unchanged original copy
private->cib_xml = saved_cib;
return rc;
}
static int
cib_file_process_commit_transaction(const char *op, int options,
const char *section, xmlNode *req,
xmlNode *input, xmlNode *existing_cib,
xmlNode **result_cib, xmlNode **answer)
{
int rc = pcmk_rc_ok;
const char *client_id = crm_element_value(req, PCMK__XA_CIB_CLIENTID);
cib_t *cib = NULL;
CRM_CHECK(client_id != NULL, return -EINVAL);
cib = get_client(client_id);
CRM_CHECK(cib != NULL, return -EINVAL);
rc = cib_file_commit_transaction(cib, input, result_cib);
if (rc != pcmk_rc_ok) {
cib_file_opaque_t *private = cib->variant_opaque;
crm_err("Could not commit transaction for CIB file client (%s) on "
"file '%s': %s",
private->id, private->filename, pcmk_rc_str(rc));
}
return pcmk_rc2legacy(rc);
}
diff --git a/lib/common/iso8601.c b/lib/common/iso8601.c
index aeb8ae9483..6e3bc3fd5e 100644
--- a/lib/common/iso8601.c
+++ b/lib/common/iso8601.c
@@ -1,2179 +1,2214 @@
/*
* Copyright 2005-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.
*/
/*
* References:
* https://en.wikipedia.org/wiki/ISO_8601
* http://www.staff.science.uu.nl/~gent0113/calendar/isocalendar.htm
*/
#include <crm_internal.h>
#include <crm/crm.h>
#include <time.h>
#include <ctype.h>
#include <inttypes.h>
#include <limits.h> // INT_MIN, INT_MAX
#include <string.h>
#include <stdbool.h>
#include <crm/common/iso8601.h>
#include <crm/common/iso8601_internal.h>
#include "crmcommon_private.h"
/*
* Andrew's code was originally written for OSes whose "struct tm" contains:
* long tm_gmtoff; :: Seconds east of UTC
* const char *tm_zone; :: Timezone abbreviation
* Some OSes lack these, instead having:
* time_t (or long) timezone;
:: "difference between UTC and local standard time"
* char *tzname[2] = { "...", "..." };
* I (David Lee) confess to not understanding the details. So my attempted
* generalisations for where their use is necessary may be flawed.
*
* 1. Does "difference between ..." subtract the same or opposite way?
* 2. Should it use "altzone" instead of "timezone"?
* 3. Should it use tzname[0] or tzname[1]? Interaction with timezone/altzone?
*/
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
# define GMTOFF(tm) ((tm)->tm_gmtoff)
#else
/* Note: extern variable; macro argument not actually used. */
# define GMTOFF(tm) (-timezone+daylight)
#endif
#define HOUR_SECONDS (60 * 60)
#define DAY_SECONDS (HOUR_SECONDS * 24)
/*!
* \internal
* \brief Validate a seconds/microseconds tuple
*
* The microseconds value must be in the correct range, and if both are nonzero
* they must have the same sign.
*
* \param[in] sec Seconds
* \param[in] usec Microseconds
*
* \return true if the seconds/microseconds tuple is valid, or false otherwise
*/
#define valid_sec_usec(sec, usec) \
((QB_ABS(usec) < QB_TIME_US_IN_SEC) \
&& (((sec) == 0) || ((usec) == 0) || (((sec) < 0) == ((usec) < 0))))
// A date/time or duration
struct crm_time_s {
int years; // Calendar year (date/time) or number of years (duration)
int months; // Number of months (duration only)
int days; // Ordinal day of year (date/time) or number of days (duration)
int seconds; // Seconds of day (date/time) or number of seconds (duration)
int offset; // Seconds offset from UTC (date/time only)
bool duration; // True if duration
};
static crm_time_t *parse_date(const char *date_str);
static crm_time_t *
crm_get_utc_time(const crm_time_t *dt)
{
crm_time_t *utc = NULL;
if (dt == NULL) {
errno = EINVAL;
return NULL;
}
utc = crm_time_new_undefined();
utc->years = dt->years;
utc->days = dt->days;
utc->seconds = dt->seconds;
utc->offset = 0;
if (dt->offset) {
crm_time_add_seconds(utc, -dt->offset);
} else {
/* Durations (which are the only things that can include months, never have a timezone */
utc->months = dt->months;
}
crm_time_log(LOG_TRACE, "utc-source", dt,
crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
crm_time_log(LOG_TRACE, "utc-target", utc,
crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
return utc;
}
crm_time_t *
crm_time_new(const char *date_time)
{
tzset();
if (date_time == NULL) {
return pcmk__copy_timet(time(NULL));
}
return parse_date(date_time);
}
/*!
* \brief Allocate memory for an uninitialized time object
*
* \return Newly allocated time object
* \note The caller is responsible for freeing the return value using
* crm_time_free().
*/
crm_time_t *
crm_time_new_undefined(void)
{
return (crm_time_t *) pcmk__assert_alloc(1, sizeof(crm_time_t));
}
/*!
* \brief Check whether a time object has been initialized yet
*
* \param[in] t Time object to check
*
* \return TRUE if time object has been initialized, FALSE otherwise
*/
bool
crm_time_is_defined(const crm_time_t *t)
{
// Any nonzero member indicates something has been done to t
return (t != NULL) && (t->years || t->months || t->days || t->seconds
|| t->offset || t->duration);
}
void
crm_time_free(crm_time_t * dt)
{
if (dt == NULL) {
return;
}
free(dt);
}
static int
year_days(int year)
{
int d = 365;
if (crm_time_leapyear(year)) {
d++;
}
return d;
}
/* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt :
*
* 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7)
* YY = (Y-1) % 100
* C = (Y-1) - YY
* G = YY + YY/4
* Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7)
*/
int
crm_time_january1_weekday(int year)
{
int YY = (year - 1) % 100;
int C = (year - 1) - YY;
int G = YY + YY / 4;
int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7);
crm_trace("YY=%d, C=%d, G=%d", YY, C, G);
crm_trace("January 1 %.4d: %d", year, jan1);
return jan1;
}
int
crm_time_weeks_in_year(int year)
{
int weeks = 52;
int jan1 = crm_time_january1_weekday(year);
/* if jan1 == thursday */
if (jan1 == 4) {
weeks++;
} else {
jan1 = crm_time_january1_weekday(year + 1);
/* if dec31 == thursday aka. jan1 of next year is a friday */
if (jan1 == 5) {
weeks++;
}
}
return weeks;
}
// Jan-Dec plus Feb of leap years
static int month_days[13] = {
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29
};
/*!
* \brief Return number of days in given month of given year
*
- * \param[in] Ordinal month (1-12)
- * \param[in] Gregorian year
+ * \param[in] month Ordinal month (1-12)
+ * \param[in] year Gregorian year
*
- * \return Number of days in given month (0 if given month is invalid)
+ * \return Number of days in given month (0 if given month or year is invalid)
*/
int
crm_time_days_in_month(int month, int year)
{
- if ((month < 1) || (month > 12)) {
+ if ((month < 1) || (month > 12) || (year < 1)) {
return 0;
}
if ((month == 2) && crm_time_leapyear(year)) {
month = 13;
}
return month_days[month - 1];
}
bool
crm_time_leapyear(int year)
{
gboolean is_leap = FALSE;
if (year % 4 == 0) {
is_leap = TRUE;
}
if (year % 100 == 0 && year % 400 != 0) {
is_leap = FALSE;
}
return is_leap;
}
-static uint32_t
+/*!
+ * \internal
+ * \brief Get ordinal day number of year corresponding to given date
+ *
+ * \param[in] y Year
+ * \param[in] m Month (1-12)
+ * \param[in] d Day of month (1-31)
+ *
+ * \return Day number of year \p y corresponding to month \p m and day \p d,
+ * or 0 for invalid arguments
+ */
+static int
get_ordinal_days(uint32_t y, uint32_t m, uint32_t d)
{
- int lpc;
+ int result = 0;
- for (lpc = 1; lpc < m; lpc++) {
- d += crm_time_days_in_month(lpc, y);
+ CRM_CHECK((y > 0) && (y <= INT_MAX) && (m >= 1) && (m <= 12)
+ && (d >= 1) && (d <= 31), return 0);
+
+ result = d;
+ for (int lpc = 1; lpc < m; lpc++) {
+ result += crm_time_days_in_month(lpc, y);
}
- return d;
+ return result;
}
void
crm_time_log_alias(int log_level, const char *file, const char *function,
int line, const char *prefix, const crm_time_t *date_time,
int flags)
{
char *date_s = crm_time_as_string(date_time, flags);
if (log_level == LOG_STDOUT) {
printf("%s%s%s\n",
(prefix? prefix : ""), (prefix? ": " : ""), date_s);
} else {
do_crm_log_alias(log_level, file, function, line, "%s%s%s",
(prefix? prefix : ""), (prefix? ": " : ""), date_s);
}
free(date_s);
}
static void
crm_time_get_sec(int sec, uint32_t *h, uint32_t *m, uint32_t *s)
{
uint32_t hours, minutes, seconds;
seconds = QB_ABS(sec);
hours = seconds / HOUR_SECONDS;
seconds -= HOUR_SECONDS * hours;
minutes = seconds / 60;
seconds -= 60 * minutes;
crm_trace("%d == %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
sec, hours, minutes, seconds);
*h = hours;
*m = minutes;
*s = seconds;
}
int
crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m,
uint32_t *s)
{
crm_time_get_sec(dt->seconds, h, m, s);
return TRUE;
}
int
crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m)
{
uint32_t s;
crm_time_get_sec(dt->seconds, h, m, &s);
return TRUE;
}
long long
crm_time_get_seconds(const crm_time_t *dt)
{
int lpc;
crm_time_t *utc = NULL;
long long in_seconds = 0;
if (dt == NULL) {
return 0;
}
utc = crm_get_utc_time(dt);
if (utc == NULL) {
return 0;
}
for (lpc = 1; lpc < utc->years; lpc++) {
long long dmax = year_days(lpc);
in_seconds += DAY_SECONDS * dmax;
}
/* utc->months is an offset that can only be set for a duration.
* By definition, the value is variable depending on the date to
* which it is applied.
*
* Force 30-day months so that something vaguely sane happens
* for anyone that tries to use a month in this way.
*/
if (utc->months > 0) {
in_seconds += DAY_SECONDS * 30 * (long long) (utc->months);
}
if (utc->days > 0) {
in_seconds += DAY_SECONDS * (long long) (utc->days - 1);
}
in_seconds += utc->seconds;
crm_time_free(utc);
return in_seconds;
}
#define EPOCH_SECONDS 62135596800ULL /* Calculated using crm_time_get_seconds() */
long long
crm_time_get_seconds_since_epoch(const crm_time_t *dt)
{
return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS);
}
int
crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m,
uint32_t *d)
{
int months = 0;
int days = dt->days;
if(dt->years != 0) {
for (months = 1; months <= 12 && days > 0; months++) {
int mdays = crm_time_days_in_month(months, dt->years);
if (mdays >= days) {
break;
} else {
days -= mdays;
}
}
} else if (dt->months) {
/* This is a duration including months, don't convert the days field */
months = dt->months;
} else {
/* This is a duration not including months, still don't convert the days field */
}
*y = dt->years;
*m = months;
*d = days;
crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days);
return TRUE;
}
int
crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d)
{
*y = dt->years;
*d = dt->days;
return TRUE;
}
int
crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w,
uint32_t *d)
{
/*
* Monday 29 December 2008 is written "2009-W01-1"
* Sunday 3 January 2010 is written "2009-W53-7"
*/
int year_num = 0;
int jan1 = crm_time_january1_weekday(dt->years);
int h = -1;
CRM_CHECK(dt->days > 0, return FALSE);
/* 6. Find the Weekday for Y M D */
h = dt->days + jan1 - 1;
*d = 1 + ((h - 1) % 7);
/* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */
if (dt->days <= (8 - jan1) && jan1 > 4) {
crm_trace("year--, jan1=%d", jan1);
year_num = dt->years - 1;
*w = crm_time_weeks_in_year(year_num);
} else {
year_num = dt->years;
}
/* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */
if (year_num == dt->years) {
int dmax = year_days(year_num);
int correction = 4 - *d;
if ((dmax - dt->days) < correction) {
crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction);
year_num = dt->years + 1;
*w = 1;
}
}
/* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */
if (year_num == dt->years) {
int j = dt->days + (7 - *d) + (jan1 - 1);
*w = j / 7;
if (jan1 > 4) {
*w -= 1;
}
}
*y = year_num;
crm_trace("Converted %.4d-%.3d to %.4" PRIu32 "-W%.2" PRIu32 "-%" PRIu32,
dt->years, dt->days, *y, *w, *d);
return TRUE;
}
#define DATE_MAX 128
/*!
* \internal
* \brief Print "<seconds>.<microseconds>" to a buffer
*
* \param[in] sec Seconds
* \param[in] usec Microseconds (must be of same sign as \p sec and of
* absolute value less than \p QB_TIME_US_IN_SEC)
* \param[in,out] buf Result buffer
* \param[in,out] offset Current offset within \p buf
*/
static inline void
sec_usec_as_string(long long sec, int usec, char *buf, size_t *offset)
{
*offset += snprintf(buf + *offset, DATE_MAX - *offset, "%s%lld.%06d",
((sec == 0) && (usec < 0))? "-" : "",
sec, QB_ABS(usec));
}
/*!
* \internal
* \brief Get a string representation of a duration
*
* \param[in] dt Time object to interpret as a duration
* \param[in] usec Microseconds to add to \p dt
* \param[in] show_usec Whether to include microseconds in \p result
* \param[out] result Where to store the result string
*/
static void
crm_duration_as_string(const crm_time_t *dt, int usec, bool show_usec,
char *result)
{
size_t offset = 0;
CRM_ASSERT(valid_sec_usec(dt->seconds, usec));
if (dt->years) {
offset += snprintf(result + offset, DATE_MAX - offset, "%4d year%s ",
dt->years, pcmk__plural_s(dt->years));
}
if (dt->months) {
offset += snprintf(result + offset, DATE_MAX - offset, "%2d month%s ",
dt->months, pcmk__plural_s(dt->months));
}
if (dt->days) {
offset += snprintf(result + offset, DATE_MAX - offset, "%2d day%s ",
dt->days, pcmk__plural_s(dt->days));
}
// At least print seconds (and optionally usecs)
if ((offset == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) {
if (show_usec) {
sec_usec_as_string(dt->seconds, usec, result, &offset);
} else {
offset += snprintf(result + offset, DATE_MAX - offset, "%d",
dt->seconds);
}
offset += snprintf(result + offset, DATE_MAX - offset, " second%s",
pcmk__plural_s(dt->seconds));
}
// More than one minute, so provide a more readable breakdown into units
if (QB_ABS(dt->seconds) >= 60) {
uint32_t h = 0;
uint32_t m = 0;
uint32_t s = 0;
uint32_t u = QB_ABS(usec);
bool print_sec_component = false;
crm_time_get_sec(dt->seconds, &h, &m, &s);
print_sec_component = ((s != 0) || (show_usec && (u != 0)));
offset += snprintf(result + offset, DATE_MAX - offset, " (");
if (h) {
offset += snprintf(result + offset, DATE_MAX - offset,
"%" PRIu32 " hour%s%s", h, pcmk__plural_s(h),
((m != 0) || print_sec_component)? " " : "");
}
if (m) {
offset += snprintf(result + offset, DATE_MAX - offset,
"%" PRIu32 " minute%s%s", m, pcmk__plural_s(m),
print_sec_component? " " : "");
}
if (print_sec_component) {
if (show_usec) {
sec_usec_as_string(s, u, result, &offset);
} else {
offset += snprintf(result + offset, DATE_MAX - offset,
"%" PRIu32, s);
}
offset += snprintf(result + offset, DATE_MAX - offset, " second%s",
pcmk__plural_s(dt->seconds));
}
offset += snprintf(result + offset, DATE_MAX - offset, ")");
}
}
/*!
* \internal
* \brief Get a string representation of a time object
*
* \param[in] dt Time to convert to string
* \param[in] usec Microseconds to add to \p dt
* \param[in] flags Group of \p crm_time_* string format options
* \param[out] result Where to store the result string
*
* \note \p result must be of size \p DATE_MAX or larger.
*/
static void
time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags,
char *result)
{
crm_time_t *utc = NULL;
size_t offset = 0;
if (!crm_time_is_defined(dt)) {
strcpy(result, "<undefined time>");
return;
}
CRM_ASSERT(valid_sec_usec(dt->seconds, usec));
/* Simple cases: as duration, seconds, or seconds since epoch.
* These never depend on time zone.
*/
if (pcmk_is_set(flags, crm_time_log_duration)) {
crm_duration_as_string(dt, usec, pcmk_is_set(flags, crm_time_usecs),
result);
return;
}
if (pcmk_any_flags_set(flags, crm_time_seconds|crm_time_epoch)) {
long long seconds = 0;
if (pcmk_is_set(flags, crm_time_seconds)) {
seconds = crm_time_get_seconds(dt);
} else {
seconds = crm_time_get_seconds_since_epoch(dt);
}
if (pcmk_is_set(flags, crm_time_usecs)) {
sec_usec_as_string(seconds, usec, result, &offset);
} else {
snprintf(result, DATE_MAX, "%lld", seconds);
}
return;
}
// Convert to UTC if local timezone was not requested
if ((dt->offset != 0) && !pcmk_is_set(flags, crm_time_log_with_timezone)) {
crm_trace("UTC conversion");
utc = crm_get_utc_time(dt);
dt = utc;
}
// As readable string
if (pcmk_is_set(flags, crm_time_log_date)) {
if (pcmk_is_set(flags, crm_time_weeks)) { // YYYY-WW-D
uint32_t y = 0;
uint32_t w = 0;
uint32_t d = 0;
if (crm_time_get_isoweek(dt, &y, &w, &d)) {
offset += snprintf(result + offset, DATE_MAX - offset,
"%" PRIu32 "-W%.2" PRIu32 "-%" PRIu32,
y, w, d);
}
} else if (pcmk_is_set(flags, crm_time_ordinal)) { // YYYY-DDD
uint32_t y = 0;
uint32_t d = 0;
if (crm_time_get_ordinal(dt, &y, &d)) {
offset += snprintf(result + offset, DATE_MAX - offset,
"%" PRIu32 "-%.3" PRIu32, y, d);
}
} else { // YYYY-MM-DD
uint32_t y = 0;
uint32_t m = 0;
uint32_t d = 0;
if (crm_time_get_gregorian(dt, &y, &m, &d)) {
offset += snprintf(result + offset, DATE_MAX - offset,
"%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32,
y, m, d);
}
}
}
if (pcmk_is_set(flags, crm_time_log_timeofday)) {
uint32_t h = 0, m = 0, s = 0;
if (offset > 0) {
offset += snprintf(result + offset, DATE_MAX - offset, " ");
}
if (crm_time_get_timeofday(dt, &h, &m, &s)) {
offset += snprintf(result + offset, DATE_MAX - offset,
"%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
h, m, s);
if (pcmk_is_set(flags, crm_time_usecs)) {
offset += snprintf(result + offset, DATE_MAX - offset,
".%06" PRIu32, QB_ABS(usec));
}
}
if (pcmk_is_set(flags, crm_time_log_with_timezone)
&& (dt->offset != 0)) {
crm_time_get_sec(dt->offset, &h, &m, &s);
offset += snprintf(result + offset, DATE_MAX - offset,
" %c%.2" PRIu32 ":%.2" PRIu32,
((dt->offset < 0)? '-' : '+'), h, m);
} else {
offset += snprintf(result + offset, DATE_MAX - offset, "Z");
}
}
crm_time_free(utc);
}
/*!
* \brief Get a string representation of a \p crm_time_t object
*
* \param[in] dt Time to convert to string
* \param[in] flags Group of \p crm_time_* string format options
*
* \note The caller is responsible for freeing the return value using \p free().
*/
char *
crm_time_as_string(const crm_time_t *dt, int flags)
{
char result[DATE_MAX] = { '\0', };
time_as_string_common(dt, 0, flags, result);
return pcmk__str_copy(result);
}
/*!
* \internal
* \brief Determine number of seconds from an hour:minute:second string
*
* \param[in] time_str Time specification string
* \param[out] result Number of seconds equivalent to time_str
*
* \return TRUE if specification was valid, FALSE (and set errno) otherwise
* \note This may return the number of seconds in a day (which is out of bounds
* for a time object) if given 24:00:00.
*/
static bool
crm_time_parse_sec(const char *time_str, int *result)
{
int rc;
uint32_t hour = 0;
uint32_t minute = 0;
uint32_t second = 0;
*result = 0;
// Must have at least hour, but minutes and seconds are optional
rc = sscanf(time_str, "%" SCNu32 ":%" SCNu32 ":%" SCNu32,
&hour, &minute, &second);
if (rc == 1) {
rc = sscanf(time_str, "%2" SCNu32 "%2" SCNu32 "%2" SCNu32,
&hour, &minute, &second);
}
if (rc == 0) {
crm_err("%s is not a valid ISO 8601 time specification", time_str);
errno = EINVAL;
return FALSE;
}
crm_trace("Got valid time: %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
hour, minute, second);
if ((hour == 24) && (minute == 0) && (second == 0)) {
// Equivalent to 00:00:00 of next day, return number of seconds in day
} else if (hour >= 24) {
crm_err("%s is not a valid ISO 8601 time specification "
"because %" PRIu32 " is not a valid hour", time_str, hour);
errno = EINVAL;
return FALSE;
}
if (minute >= 60) {
crm_err("%s is not a valid ISO 8601 time specification "
"because %" PRIu32 " is not a valid minute", time_str, minute);
errno = EINVAL;
return FALSE;
}
if (second >= 60) {
crm_err("%s is not a valid ISO 8601 time specification "
"because %" PRIu32 " is not a valid second", time_str, second);
errno = EINVAL;
return FALSE;
}
*result = (hour * HOUR_SECONDS) + (minute * 60) + second;
return TRUE;
}
static bool
crm_time_parse_offset(const char *offset_str, int *offset)
{
tzset();
if (offset_str == NULL) {
// Use local offset
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
time_t now = time(NULL);
struct tm *now_tm = localtime(&now);
#endif
int h_offset = GMTOFF(now_tm) / HOUR_SECONDS;
int m_offset = (GMTOFF(now_tm) - (HOUR_SECONDS * h_offset)) / 60;
if (h_offset < 0 && m_offset < 0) {
m_offset = 0 - m_offset;
}
*offset = (HOUR_SECONDS * h_offset) + (60 * m_offset);
return TRUE;
}
if (offset_str[0] == 'Z') { // @TODO invalid if anything after?
*offset = 0;
return TRUE;
}
*offset = 0;
if ((offset_str[0] == '+') || (offset_str[0] == '-')
|| isdigit((int)offset_str[0])) {
gboolean negate = FALSE;
if (offset_str[0] == '+') {
offset_str++;
} else if (offset_str[0] == '-') {
negate = TRUE;
offset_str++;
}
if (crm_time_parse_sec(offset_str, offset) == FALSE) {
return FALSE;
}
if (negate) {
*offset = 0 - *offset;
}
} // @TODO else invalid?
return TRUE;
}
/*!
* \internal
* \brief Parse the time portion of an ISO 8601 date/time string
*
* \param[in] time_str Time portion of specification (after any 'T')
* \param[in,out] a_time Time object to parse into
*
* \return TRUE if valid time was parsed, FALSE (and set errno) otherwise
* \note This may add a day to a_time (if the time is 24:00:00).
*/
static bool
crm_time_parse(const char *time_str, crm_time_t *a_time)
{
uint32_t h, m, s;
char *offset_s = NULL;
tzset();
if (time_str) {
if (crm_time_parse_sec(time_str, &(a_time->seconds)) == FALSE) {
return FALSE;
}
offset_s = strstr(time_str, "Z");
if (offset_s == NULL) {
offset_s = strstr(time_str, " ");
if (offset_s) {
while (isspace(offset_s[0])) {
offset_s++;
}
}
}
}
if (crm_time_parse_offset(offset_s, &(a_time->offset)) == FALSE) {
return FALSE;
}
crm_time_get_sec(a_time->offset, &h, &m, &s);
crm_trace("Got tz: %c%2." PRIu32 ":%.2" PRIu32,
(a_time->offset < 0)? '-' : '+', h, m);
if (a_time->seconds == DAY_SECONDS) {
// 24:00:00 == 00:00:00 of next day
a_time->seconds = 0;
crm_time_add_days(a_time, 1);
}
return TRUE;
}
/*
* \internal
* \brief Parse a time object from an ISO 8601 date/time specification
*
* \param[in] date_str ISO 8601 date/time specification (or
* \c PCMK__VALUE_EPOCH)
*
* \return New time object on success, NULL (and set errno) otherwise
*/
static crm_time_t *
parse_date(const char *date_str)
{
const char *time_s = NULL;
crm_time_t *dt = NULL;
- int year = 0;
- int month = 0;
- int week = 0;
- int day = 0;
+ uint32_t year = 0U;
+ uint32_t month = 0U;
+ uint32_t day = 0U;
+ uint32_t week = 0U;
+
int rc = 0;
if (pcmk__str_empty(date_str)) {
crm_err("No ISO 8601 date/time specification given");
goto invalid;
}
if ((date_str[0] == 'T')
|| ((strlen(date_str) > 2) && (date_str[2] == ':'))) {
/* Just a time supplied - Infer current date */
dt = crm_time_new(NULL);
if (date_str[0] == 'T') {
time_s = date_str + 1;
} else {
time_s = date_str;
}
goto parse_time;
}
dt = crm_time_new_undefined();
if ((strncasecmp(PCMK__VALUE_EPOCH, date_str, 5) == 0)
&& ((date_str[5] == '\0')
|| (date_str[5] == '/')
|| isspace(date_str[5]))) {
dt->days = 1;
dt->years = 1970;
crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
return dt;
}
/* YYYY-MM-DD */
- rc = sscanf(date_str, "%d-%d-%d", &year, &month, &day);
+ rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32 "-%" SCNu32 "",
+ &year, &month, &day);
if (rc == 1) {
/* YYYYMMDD */
- rc = sscanf(date_str, "%4d%2d%2d", &year, &month, &day);
+ rc = sscanf(date_str, "%4" SCNu32 "%2" SCNu32 "%2" SCNu32 "",
+ &year, &month, &day);
}
if (rc == 3) {
- if (month > 12) {
+ if ((month < 1U) || (month > 12U)) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
- "because '%d' is not a valid month", date_str, month);
+ "because '%" PRIu32 "' is not a valid month",
+ date_str, month);
goto invalid;
- } else if (day > crm_time_days_in_month(month, year)) {
+ } else if ((year < 1U) || (year > INT_MAX)) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
- "because '%d' is not a valid day of the month",
+ "because '%" PRIu32 "' is not a valid year",
+ date_str, year);
+ goto invalid;
+ } else if ((day < 1) || (day > INT_MAX)
+ || (day > crm_time_days_in_month(month, year))) {
+ crm_err("'%s' is not a valid ISO 8601 date/time specification "
+ "because '%" PRIu32 "' is not a valid day of the month",
date_str, day);
goto invalid;
} else {
dt->years = year;
dt->days = get_ordinal_days(year, month, day);
- crm_trace("Parsed Gregorian date '%.4d-%.3d' from date string '%s'",
- year, dt->days, date_str);
+ crm_trace("Parsed Gregorian date '%.4" PRIu32 "-%.3d' "
+ "from date string '%s'", year, dt->days, date_str);
}
goto parse_time;
}
/* YYYY-DDD */
- rc = sscanf(date_str, "%d-%d", &year, &day);
+ rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32, &year, &day);
if (rc == 2) {
- if (day > year_days(year)) {
+ if ((year < 1U) || (year > INT_MAX)) {
+ crm_err("'%s' is not a valid ISO 8601 date/time specification "
+ "because '%" PRIu32 "' is not a valid year",
+ date_str, year);
+ goto invalid;
+ } else if ((day < 1U) || (day > INT_MAX) || (day > year_days(year))) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
- "because '%d' is not a valid day of the year (max %d)",
- date_str, day, year_days(year));
+ "because '%" PRIu32 "' is not a valid day of year %"
+ PRIu32 " (1-%d)",
+ date_str, day, year, year_days(year));
goto invalid;
}
crm_trace("Parsed ordinal year %d and days %d from date string '%s'",
year, day, date_str);
dt->days = day;
dt->years = year;
goto parse_time;
}
/* YYYY-Www-D */
- rc = sscanf(date_str, "%d-W%d-%d", &year, &week, &day);
+ rc = sscanf(date_str, "%" SCNu32 "-W%" SCNu32 "-%" SCNu32,
+ &year, &week, &day);
if (rc == 3) {
- if (week > crm_time_weeks_in_year(year)) {
+ if ((week < 1U) || (week > crm_time_weeks_in_year(year))) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
- "because '%d' is not a valid week of the year (max %d)",
- date_str, week, crm_time_weeks_in_year(year));
+ "because '%" PRIu32 "' is not a valid week of year %"
+ PRIu32 " (1-%d)",
+ date_str, week, year, crm_time_weeks_in_year(year));
goto invalid;
- } else if (day < 1 || day > 7) {
+ } else if ((day < 1U) || (day > 7U)) {
crm_err("'%s' is not a valid ISO 8601 date/time specification "
- "because '%d' is not a valid day of the week",
+ "because '%" PRIu32 "' is not a valid day of the week",
date_str, day);
goto invalid;
} else {
/*
* See https://en.wikipedia.org/wiki/ISO_week_date
*
* Monday 29 December 2008 is written "2009-W01-1"
* Sunday 3 January 2010 is written "2009-W53-7"
* Saturday 27 September 2008 is written "2008-W37-6"
*
- * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in week 01.
- * If 1 January is on a Friday, Saturday or Sunday, it is in week 52 or 53 of the previous year.
+ * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it
+ * is in week 1. If 1 January is on a Friday, Saturday or Sunday,
+ * it is in week 52 or 53 of the previous year.
*/
int jan1 = crm_time_january1_weekday(year);
- crm_trace("Got year %d (Jan 1 = %d), week %d, and day %d from date string '%s'",
+ crm_trace("Parsed year %" PRIu32 " (Jan 1 = %d), week %" PRIu32
+ ", and day %" PRIu32 " from date string '%s'",
year, jan1, week, day, date_str);
dt->years = year;
crm_time_add_days(dt, (week - 1) * 7);
if (jan1 <= 4) {
crm_time_add_days(dt, 1 - jan1);
} else {
crm_time_add_days(dt, 8 - jan1);
}
crm_time_add_days(dt, day);
}
goto parse_time;
}
crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str);
goto invalid;
parse_time:
if (time_s == NULL) {
time_s = date_str + strspn(date_str, "0123456789-W");
if ((time_s[0] == ' ') || (time_s[0] == 'T')) {
++time_s;
} else {
time_s = NULL;
}
}
if ((time_s != NULL) && (crm_time_parse(time_s, dt) == FALSE)) {
goto invalid;
}
crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
if (crm_time_check(dt) == FALSE) {
crm_err("'%s' is not a valid ISO 8601 date/time specification",
date_str);
goto invalid;
}
return dt;
invalid:
crm_time_free(dt);
errno = EINVAL;
return NULL;
}
// Parse an ISO 8601 numeric value and return number of characters consumed
static int
parse_int(const char *str, int *result)
{
unsigned int lpc;
int offset = (str[0] == 'T')? 1 : 0;
bool negate = false;
*result = 0;
// @TODO This cannot handle combinations of these characters
switch (str[offset]) {
case '.':
case ',':
return 0; // Fractions are not supported
case '-':
negate = true;
offset++;
break;
case '+':
case ':':
offset++;
break;
default:
break;
}
for (lpc = 0; (lpc < 10) && isdigit(str[offset]); lpc++) {
const int digit = str[offset++] - '0';
if ((*result * 10LL + digit) > INT_MAX) {
return 0; // Overflow
}
*result = *result * 10 + digit;
}
if (negate) {
*result = 0 - *result;
}
return (lpc > 0)? offset : 0;
}
/*!
* \brief Parse a time duration from an ISO 8601 duration specification
*
* \param[in] period_s ISO 8601 duration specification (optionally followed by
* whitespace, after which the rest of the string will be
* ignored)
*
* \return New time object on success, NULL (and set errno) otherwise
* \note It is the caller's responsibility to return the result using
* crm_time_free().
*/
crm_time_t *
crm_time_parse_duration(const char *period_s)
{
gboolean is_time = FALSE;
crm_time_t *diff = NULL;
if (pcmk__str_empty(period_s)) {
crm_err("No ISO 8601 time duration given");
goto invalid;
}
if (period_s[0] != 'P') {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because it does not start with a 'P'", period_s);
goto invalid;
}
if ((period_s[1] == '\0') || isspace(period_s[1])) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because nothing follows 'P'", period_s);
goto invalid;
}
diff = crm_time_new_undefined();
for (const char *current = period_s + 1;
current[0] && (current[0] != '/') && !isspace(current[0]);
++current) {
int an_int = 0, rc;
if (current[0] == 'T') {
/* A 'T' separates year/month/day from hour/minute/seconds. We don't
* require it strictly, but just use it to differentiate month from
* minutes.
*/
is_time = TRUE;
continue;
}
// An integer must be next
rc = parse_int(current, &an_int);
if (rc == 0) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because no valid integer at '%s'", period_s, current);
goto invalid;
}
current += rc;
// A time unit must be next (we're not strict about the order)
switch (current[0]) {
case 'Y':
diff->years = an_int;
break;
case 'M':
if (!is_time) { // Months
diff->months = an_int;
// Minutes
} else if ((diff->seconds + (an_int * 60LL)) > INT_MAX) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because integer at '%s' is too large",
period_s, current - rc);
goto invalid;
} else {
diff->seconds += an_int * 60;
}
break;
case 'W':
if ((diff->days + (an_int * 7LL)) > INT_MAX) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because integer at '%s' is too large",
period_s, current - rc);
goto invalid;
} else {
diff->days += an_int * 7;
}
break;
case 'D':
if ((diff->days + (long long) an_int) > INT_MAX) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because integer at '%s' is too large",
period_s, current - rc);
goto invalid;
} else {
diff->days += an_int;
}
break;
case 'H':
if ((diff->seconds + ((long long) an_int * HOUR_SECONDS))
> INT_MAX) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because integer at '%s' is too large",
period_s, current - rc);
goto invalid;
} else {
diff->seconds += an_int * HOUR_SECONDS;
}
break;
case 'S':
if ((diff->seconds + (long long) an_int) > INT_MAX) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because integer at '%s' is too large",
period_s, current - rc);
goto invalid;
} else {
diff->seconds += an_int;
}
break;
case '\0':
crm_err("'%s' is not a valid ISO 8601 time duration "
"because no units after %d", period_s, an_int);
goto invalid;
default:
crm_err("'%s' is not a valid ISO 8601 time duration "
"because '%c' is not a valid time unit",
period_s, current[0]);
goto invalid;
}
}
if (!crm_time_is_defined(diff)) {
crm_err("'%s' is not a valid ISO 8601 time duration "
"because no amounts and units given", period_s);
goto invalid;
}
diff->duration = TRUE;
return diff;
invalid:
crm_time_free(diff);
errno = EINVAL;
return NULL;
}
/*!
* \brief Parse a time period from an ISO 8601 interval specification
*
* \param[in] period_str ISO 8601 interval specification (start/end,
* start/duration, or duration/end)
*
* \return New time period object on success, NULL (and set errno) otherwise
* \note The caller is responsible for freeing the result using
* crm_time_free_period().
*/
crm_time_period_t *
crm_time_parse_period(const char *period_str)
{
const char *original = period_str;
crm_time_period_t *period = NULL;
if (pcmk__str_empty(period_str)) {
crm_err("No ISO 8601 time period given");
goto invalid;
}
tzset();
period = pcmk__assert_alloc(1, sizeof(crm_time_period_t));
if (period_str[0] == 'P') {
period->diff = crm_time_parse_duration(period_str);
if (period->diff == NULL) {
goto error;
}
} else {
period->start = parse_date(period_str);
if (period->start == NULL) {
goto error;
}
}
period_str = strstr(original, "/");
if (period_str) {
++period_str;
if (period_str[0] == 'P') {
if (period->diff != NULL) {
crm_err("'%s' is not a valid ISO 8601 time period "
"because it has two durations",
original);
goto invalid;
}
period->diff = crm_time_parse_duration(period_str);
if (period->diff == NULL) {
goto error;
}
} else {
period->end = parse_date(period_str);
if (period->end == NULL) {
goto error;
}
}
} else if (period->diff != NULL) {
// Only duration given, assume start is now
period->start = crm_time_new(NULL);
} else {
// Only start given
crm_err("'%s' is not a valid ISO 8601 time period "
"because it has no duration or ending time",
original);
goto invalid;
}
if (period->start == NULL) {
period->start = crm_time_subtract(period->end, period->diff);
} else if (period->end == NULL) {
period->end = crm_time_add(period->start, period->diff);
}
if (crm_time_check(period->start) == FALSE) {
crm_err("'%s' is not a valid ISO 8601 time period "
"because the start is invalid", period_str);
goto invalid;
}
if (crm_time_check(period->end) == FALSE) {
crm_err("'%s' is not a valid ISO 8601 time period "
"because the end is invalid", period_str);
goto invalid;
}
return period;
invalid:
errno = EINVAL;
error:
crm_time_free_period(period);
return NULL;
}
/*!
* \brief Free a dynamically allocated time period object
*
* \param[in,out] period Time period to free
*/
void
crm_time_free_period(crm_time_period_t *period)
{
if (period) {
crm_time_free(period->start);
crm_time_free(period->end);
crm_time_free(period->diff);
free(period);
}
}
void
crm_time_set(crm_time_t *target, const crm_time_t *source)
{
crm_trace("target=%p, source=%p", target, source);
CRM_CHECK(target != NULL && source != NULL, return);
target->years = source->years;
target->days = source->days;
target->months = source->months; /* Only for durations */
target->seconds = source->seconds;
target->offset = source->offset;
crm_time_log(LOG_TRACE, "source", source,
crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
crm_time_log(LOG_TRACE, "target", target,
crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
}
static void
ha_set_tm_time(crm_time_t *target, const struct tm *source)
{
int h_offset = 0;
int m_offset = 0;
/* Ensure target is fully initialized */
target->years = 0;
target->months = 0;
target->days = 0;
target->seconds = 0;
target->offset = 0;
target->duration = FALSE;
if (source->tm_year > 0) {
/* years since 1900 */
target->years = 1900 + source->tm_year;
}
if (source->tm_yday >= 0) {
/* days since January 1 [0-365] */
target->days = 1 + source->tm_yday;
}
if (source->tm_hour >= 0) {
target->seconds += HOUR_SECONDS * source->tm_hour;
}
if (source->tm_min >= 0) {
target->seconds += 60 * source->tm_min;
}
if (source->tm_sec >= 0) {
target->seconds += source->tm_sec;
}
/* tm_gmtoff == offset from UTC in seconds */
h_offset = GMTOFF(source) / HOUR_SECONDS;
m_offset = (GMTOFF(source) - (HOUR_SECONDS * h_offset)) / 60;
crm_trace("Time offset is %lds (%.2d:%.2d)",
GMTOFF(source), h_offset, m_offset);
target->offset += HOUR_SECONDS * h_offset;
target->offset += 60 * m_offset;
}
void
crm_time_set_timet(crm_time_t *target, const time_t *source)
{
ha_set_tm_time(target, localtime(source));
}
/*!
* \internal
* \brief Set one time object to another if the other is earlier
*
* \param[in,out] target Time object to set
* \param[in] source Time object to use if earlier
*/
void
pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source)
{
if ((target != NULL) && (source != NULL)
&& (!crm_time_is_defined(target)
|| (crm_time_compare(source, target) < 0))) {
crm_time_set(target, source);
}
}
crm_time_t *
pcmk_copy_time(const crm_time_t *source)
{
crm_time_t *target = crm_time_new_undefined();
crm_time_set(target, source);
return target;
}
/*!
* \internal
* \brief Convert a \p time_t time to a \p crm_time_t time
*
* \param[in] source Time to convert
*
* \return A \p crm_time_t object representing \p source
*/
crm_time_t *
pcmk__copy_timet(time_t source)
{
crm_time_t *target = crm_time_new_undefined();
crm_time_set_timet(target, &source);
return target;
}
crm_time_t *
crm_time_add(const crm_time_t *dt, const crm_time_t *value)
{
crm_time_t *utc = NULL;
crm_time_t *answer = NULL;
if ((dt == NULL) || (value == NULL)) {
errno = EINVAL;
return NULL;
}
answer = pcmk_copy_time(dt);
utc = crm_get_utc_time(value);
if (utc == NULL) {
crm_time_free(answer);
return NULL;
}
answer->years += utc->years;
crm_time_add_months(answer, utc->months);
crm_time_add_days(answer, utc->days);
crm_time_add_seconds(answer, utc->seconds);
crm_time_free(utc);
return answer;
}
/*!
* \internal
* \brief Return the XML attribute name corresponding to a time component
*
* \param[in] component Component to check
*
* \return XML attribute name corresponding to \p component, or NULL if
* \p component is invalid
*/
const char *
pcmk__time_component_attr(enum pcmk__time_component component)
{
switch (component) {
case pcmk__time_years:
return PCMK_XA_YEARS;
case pcmk__time_months:
return PCMK_XA_MONTHS;
case pcmk__time_weeks:
return PCMK_XA_WEEKS;
case pcmk__time_days:
return PCMK_XA_DAYS;
case pcmk__time_hours:
return PCMK_XA_HOURS;
case pcmk__time_minutes:
return PCMK_XA_MINUTES;
case pcmk__time_seconds:
return PCMK_XA_SECONDS;
default:
return NULL;
}
}
typedef void (*component_fn_t)(crm_time_t *, int);
/*!
* \internal
* \brief Get the addition function corresponding to a time component
* \param[in] component Component to check
*
* \return Addition function corresponding to \p component, or NULL if
* \p component is invalid
*/
static component_fn_t
component_fn(enum pcmk__time_component component)
{
switch (component) {
case pcmk__time_years:
return crm_time_add_years;
case pcmk__time_months:
return crm_time_add_months;
case pcmk__time_weeks:
return crm_time_add_weeks;
case pcmk__time_days:
return crm_time_add_days;
case pcmk__time_hours:
return crm_time_add_hours;
case pcmk__time_minutes:
return crm_time_add_minutes;
case pcmk__time_seconds:
return crm_time_add_seconds;
default:
return NULL;
}
}
/*!
* \internal
* \brief Add the value of an XML attribute to a time object
*
* \param[in,out] t Time object to add to
* \param[in] component Component of \p t to add to
* \param[in] xml XML with value to add
*
* \return Standard Pacemaker return code
*/
int
pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component,
const xmlNode *xml)
{
long long value;
const char *attr = pcmk__time_component_attr(component);
component_fn_t add = component_fn(component);
if ((t == NULL) || (attr == NULL) || (add == NULL)) {
return EINVAL;
}
if (xml == NULL) {
return pcmk_rc_ok;
}
if (pcmk__scan_ll(crm_element_value(xml, attr), &value,
0LL) != pcmk_rc_ok) {
return pcmk_rc_unpack_error;
}
if ((value < INT_MIN) || (value > INT_MAX)) {
return ERANGE;
}
if (value != 0LL) {
add(t, (int) value);
}
return pcmk_rc_ok;
}
crm_time_t *
crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value)
{
crm_time_t *utc = NULL;
crm_time_t *answer = NULL;
if ((dt == NULL) || (value == NULL)) {
errno = EINVAL;
return NULL;
}
utc = crm_get_utc_time(value);
if (utc == NULL) {
return NULL;
}
answer = crm_get_utc_time(dt);
if (answer == NULL) {
crm_time_free(utc);
return NULL;
}
answer->duration = TRUE;
answer->years -= utc->years;
if(utc->months != 0) {
crm_time_add_months(answer, -utc->months);
}
crm_time_add_days(answer, -utc->days);
crm_time_add_seconds(answer, -utc->seconds);
crm_time_free(utc);
return answer;
}
crm_time_t *
crm_time_subtract(const crm_time_t *dt, const crm_time_t *value)
{
crm_time_t *utc = NULL;
crm_time_t *answer = NULL;
if ((dt == NULL) || (value == NULL)) {
errno = EINVAL;
return NULL;
}
utc = crm_get_utc_time(value);
if (utc == NULL) {
return NULL;
}
answer = pcmk_copy_time(dt);
answer->years -= utc->years;
if(utc->months != 0) {
crm_time_add_months(answer, -utc->months);
}
crm_time_add_days(answer, -utc->days);
crm_time_add_seconds(answer, -utc->seconds);
crm_time_free(utc);
return answer;
}
/*!
* \brief Check whether a time object represents a sensible date/time
*
* \param[in] dt Date/time object to check
*
* \return \c true if years, days, and seconds are sensible, \c false otherwise
*/
bool
crm_time_check(const crm_time_t *dt)
{
return (dt != NULL)
&& (dt->days > 0) && (dt->days <= year_days(dt->years))
&& (dt->seconds >= 0) && (dt->seconds < DAY_SECONDS);
}
#define do_cmp_field(l, r, field) \
if(rc == 0) { \
if(l->field > r->field) { \
crm_trace("%s: %d > %d", \
#field, l->field, r->field); \
rc = 1; \
} else if(l->field < r->field) { \
crm_trace("%s: %d < %d", \
#field, l->field, r->field); \
rc = -1; \
} \
}
int
crm_time_compare(const crm_time_t *a, const crm_time_t *b)
{
int rc = 0;
crm_time_t *t1 = crm_get_utc_time(a);
crm_time_t *t2 = crm_get_utc_time(b);
if ((t1 == NULL) && (t2 == NULL)) {
rc = 0;
} else if (t1 == NULL) {
rc = -1;
} else if (t2 == NULL) {
rc = 1;
} else {
do_cmp_field(t1, t2, years);
do_cmp_field(t1, t2, days);
do_cmp_field(t1, t2, seconds);
}
crm_time_free(t1);
crm_time_free(t2);
return rc;
}
/*!
* \brief Add a given number of seconds to a date/time or duration
*
* \param[in,out] a_time Date/time or duration to add seconds to
* \param[in] extra Number of seconds to add
*/
void
crm_time_add_seconds(crm_time_t *a_time, int extra)
{
int days = 0;
crm_trace("Adding %d seconds to %d (max=%d)",
extra, a_time->seconds, DAY_SECONDS);
a_time->seconds += extra;
days = a_time->seconds / DAY_SECONDS;
a_time->seconds %= DAY_SECONDS;
// Don't have negative seconds
if (a_time->seconds < 0) {
a_time->seconds += DAY_SECONDS;
--days;
}
crm_time_add_days(a_time, days);
}
void
crm_time_add_days(crm_time_t * a_time, int extra)
{
int lower_bound = 1;
int ydays = crm_time_leapyear(a_time->years) ? 366 : 365;
crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days);
a_time->days += extra;
while (a_time->days > ydays) {
a_time->years++;
a_time->days -= ydays;
ydays = crm_time_leapyear(a_time->years) ? 366 : 365;
}
if(a_time->duration) {
lower_bound = 0;
}
while (a_time->days < lower_bound) {
a_time->years--;
a_time->days += crm_time_leapyear(a_time->years) ? 366 : 365;
}
}
void
crm_time_add_months(crm_time_t * a_time, int extra)
{
int lpc;
uint32_t y, m, d, dmax;
crm_time_get_gregorian(a_time, &y, &m, &d);
crm_trace("Adding %d months to %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32,
extra, y, m, d);
if (extra > 0) {
for (lpc = extra; lpc > 0; lpc--) {
m++;
if (m == 13) {
m = 1;
y++;
}
}
} else {
for (lpc = -extra; lpc > 0; lpc--) {
m--;
if (m == 0) {
m = 12;
y--;
}
}
}
dmax = crm_time_days_in_month(m, y);
if (dmax < d) {
/* Preserve day-of-month unless the month doesn't have enough days */
d = dmax;
}
crm_trace("Calculated %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d);
a_time->years = y;
a_time->days = get_ordinal_days(y, m, d);
crm_time_get_gregorian(a_time, &y, &m, &d);
crm_trace("Got %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d);
}
void
crm_time_add_minutes(crm_time_t * a_time, int extra)
{
crm_time_add_seconds(a_time, extra * 60);
}
void
crm_time_add_hours(crm_time_t * a_time, int extra)
{
crm_time_add_seconds(a_time, extra * HOUR_SECONDS);
}
void
crm_time_add_weeks(crm_time_t * a_time, int extra)
{
crm_time_add_days(a_time, extra * 7);
}
void
crm_time_add_years(crm_time_t * a_time, int extra)
{
a_time->years += extra;
}
static void
ha_get_tm_time(struct tm *target, const crm_time_t *source)
{
*target = (struct tm) {
.tm_year = source->years - 1900,
.tm_mday = source->days,
.tm_sec = source->seconds % 60,
.tm_min = ( source->seconds / 60 ) % 60,
.tm_hour = source->seconds / HOUR_SECONDS,
.tm_isdst = -1, /* don't adjust */
#if defined(HAVE_STRUCT_TM_TM_GMTOFF)
.tm_gmtoff = source->offset
#endif
};
mktime(target);
}
/* The high-resolution variant of time object was added to meet an immediate
* need, and is kept internal API.
*
* @TODO The long-term goal is to come up with a clean, unified design for a
* time type (or types) that meets all the various needs, to replace
* crm_time_t, pcmk__time_hr_t, and struct timespec (in lrmd_cmd_t).
* Using glib's GDateTime is a possibility (if we are willing to require
* glib >= 2.26).
*/
pcmk__time_hr_t *
pcmk__time_hr_convert(pcmk__time_hr_t *target, const crm_time_t *dt)
{
pcmk__time_hr_t *hr_dt = NULL;
if (dt) {
hr_dt = target;
if (hr_dt == NULL) {
hr_dt = pcmk__assert_alloc(1, sizeof(pcmk__time_hr_t));
}
*hr_dt = (pcmk__time_hr_t) {
.years = dt->years,
.months = dt->months,
.days = dt->days,
.seconds = dt->seconds,
.offset = dt->offset,
.duration = dt->duration
};
}
return hr_dt;
}
void
pcmk__time_set_hr_dt(crm_time_t *target, const pcmk__time_hr_t *hr_dt)
{
CRM_ASSERT((hr_dt) && (target));
*target = (crm_time_t) {
.years = hr_dt->years,
.months = hr_dt->months,
.days = hr_dt->days,
.seconds = hr_dt->seconds,
.offset = hr_dt->offset,
.duration = hr_dt->duration
};
}
/*!
* \internal
* \brief Return the current time as a high-resolution time
*
* \param[out] epoch If not NULL, this will be set to seconds since epoch
*
* \return Newly allocated high-resolution time set to the current time
*/
pcmk__time_hr_t *
pcmk__time_hr_now(time_t *epoch)
{
struct timespec tv;
crm_time_t dt;
pcmk__time_hr_t *hr;
qb_util_timespec_from_epoch_get(&tv);
if (epoch != NULL) {
*epoch = tv.tv_sec;
}
crm_time_set_timet(&dt, &(tv.tv_sec));
hr = pcmk__time_hr_convert(NULL, &dt);
if (hr != NULL) {
hr->useconds = tv.tv_nsec / QB_TIME_NS_IN_USEC;
}
return hr;
}
pcmk__time_hr_t *
pcmk__time_hr_new(const char *date_time)
{
pcmk__time_hr_t *hr_dt = NULL;
if (date_time == NULL) {
hr_dt = pcmk__time_hr_now(NULL);
} else {
crm_time_t *dt;
dt = parse_date(date_time);
hr_dt = pcmk__time_hr_convert(NULL, dt);
crm_time_free(dt);
}
return hr_dt;
}
void
pcmk__time_hr_free(pcmk__time_hr_t * hr_dt)
{
free(hr_dt);
}
/*!
* \internal
* \brief Expand a date/time format string, including %N for nanoseconds
*
* \param[in] format Date/time format string as per strftime(3) with the
* addition of %N for nanoseconds
* \param[in] hr_dt Time value to format
*
* \return Newly allocated string with formatted string
*/
char *
pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt)
{
int scanned_pos = 0; // How many characters of format have been parsed
int printed_pos = 0; // How many characters of format have been processed
size_t date_len = 0;
char nano_s[10] = { '\0', };
char date_s[128] = { '\0', };
struct tm tm = { 0, };
crm_time_t dt = { 0, };
if (format == NULL) {
return NULL;
}
pcmk__time_set_hr_dt(&dt, hr_dt);
ha_get_tm_time(&tm, &dt);
sprintf(nano_s, "%06d000", hr_dt->useconds);
while (format[scanned_pos] != '\0') {
int fmt_pos; // Index after last character to pass as-is
int nano_digits = 0; // Length of %N field width (if any)
char *tmp_fmt_s = NULL;
size_t nbytes = 0;
// Look for next format specifier
const char *mark_s = strchr(&format[scanned_pos], '%');
if (mark_s == NULL) {
// No more specifiers, so pass remaining string to strftime() as-is
scanned_pos = strlen(format);
fmt_pos = scanned_pos;
} else {
fmt_pos = mark_s - format; // Index of %
// Skip % and any field width
scanned_pos = fmt_pos + 1;
while (isdigit(format[scanned_pos])) {
scanned_pos++;
}
switch (format[scanned_pos]) {
case '\0': // Literal % and possibly digits at end of string
fmt_pos = scanned_pos; // Pass remaining string as-is
break;
case 'N': // %[width]N
scanned_pos++;
// Parse field width
nano_digits = atoi(&format[fmt_pos + 1]);
nano_digits = QB_MAX(nano_digits, 0);
nano_digits = QB_MIN(nano_digits, 6);
break;
default: // Some other specifier
if (format[++scanned_pos] != '\0') { // More to parse
continue;
}
fmt_pos = scanned_pos; // Pass remaining string as-is
break;
}
}
if (date_len >= sizeof(date_s)) {
return NULL; // No room for remaining string
}
tmp_fmt_s = strndup(&format[printed_pos], fmt_pos - printed_pos);
#ifdef HAVE_FORMAT_NONLITERAL
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
nbytes = strftime(&date_s[date_len], sizeof(date_s) - date_len,
tmp_fmt_s, &tm);
#ifdef HAVE_FORMAT_NONLITERAL
#pragma GCC diagnostic pop
#endif
free(tmp_fmt_s);
if (nbytes == 0) { // Would overflow buffer
return NULL;
}
date_len += nbytes;
printed_pos = scanned_pos;
if (nano_digits != 0) {
int nc = 0;
if (date_len >= sizeof(date_s)) {
return NULL; // No room to add nanoseconds
}
nc = snprintf(&date_s[date_len], sizeof(date_s) - date_len,
"%.*s", nano_digits, nano_s);
if ((nc < 0) || (nc == (sizeof(date_s) - date_len))) {
return NULL; // Error or would overflow buffer
}
date_len += nc;
}
}
return (date_len == 0)? NULL : pcmk__str_copy(date_s);
}
/*!
* \internal
* \brief Return a human-friendly string corresponding to an epoch time value
*
* \param[in] source Pointer to epoch time value (or \p NULL for current time)
* \param[in] flags Group of \p crm_time_* flags controlling display format
* (0 to use \p ctime() with newline removed)
*
* \return String representation of \p source on success (may be empty depending
* on \p flags; guaranteed not to be \p NULL)
*
* \note The caller is responsible for freeing the return value using \p free().
*/
char *
pcmk__epoch2str(const time_t *source, uint32_t flags)
{
time_t epoch_time = (source == NULL)? time(NULL) : *source;
if (flags == 0) {
return pcmk__str_copy(pcmk__trim(ctime(&epoch_time)));
} else {
crm_time_t dt;
crm_time_set_timet(&dt, &epoch_time);
return crm_time_as_string(&dt, flags);
}
}
/*!
* \internal
* \brief Return a human-friendly string corresponding to seconds-and-
* nanoseconds value
*
* Time is shown with microsecond resolution if \p crm_time_usecs is in \p
* flags.
*
* \param[in] ts Time in seconds and nanoseconds (or \p NULL for current
* time)
* \param[in] flags Group of \p crm_time_* flags controlling display format
*
* \return String representation of \p ts on success (may be empty depending on
* \p flags; guaranteed not to be \p NULL)
*
* \note The caller is responsible for freeing the return value using \p free().
*/
char *
pcmk__timespec2str(const struct timespec *ts, uint32_t flags)
{
struct timespec tmp_ts;
crm_time_t dt;
char result[DATE_MAX] = { 0 };
if (ts == NULL) {
qb_util_timespec_from_epoch_get(&tmp_ts);
ts = &tmp_ts;
}
crm_time_set_timet(&dt, &ts->tv_sec);
time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags, result);
return pcmk__str_copy(result);
}
/*!
* \internal
* \brief Given a millisecond interval, return a log-friendly string
*
* \param[in] interval_ms Interval in milliseconds
*
* \return Readable version of \p interval_ms
*
* \note The return value is a pointer to static memory that will be
* overwritten by later calls to this function.
*/
const char *
pcmk__readable_interval(guint interval_ms)
{
#define MS_IN_S (1000)
#define MS_IN_M (MS_IN_S * 60)
#define MS_IN_H (MS_IN_M * 60)
#define MS_IN_D (MS_IN_H * 24)
#define MAXSTR sizeof("..d..h..m..s...ms")
static char str[MAXSTR];
int offset = 0;
str[0] = '\0';
if (interval_ms >= MS_IN_D) {
offset += snprintf(str + offset, MAXSTR - offset, "%ud",
interval_ms / MS_IN_D);
interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D;
}
if (interval_ms >= MS_IN_H) {
offset += snprintf(str + offset, MAXSTR - offset, "%uh",
interval_ms / MS_IN_H);
interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H;
}
if (interval_ms >= MS_IN_M) {
offset += snprintf(str + offset, MAXSTR - offset, "%um",
interval_ms / MS_IN_M);
interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M;
}
// Ns, N.NNNs, or NNNms
if (interval_ms >= MS_IN_S) {
offset += snprintf(str + offset, MAXSTR - offset, "%u",
interval_ms / MS_IN_S);
interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S;
if (interval_ms > 0) {
offset += snprintf(str + offset, MAXSTR - offset, ".%03u",
interval_ms);
}
(void) snprintf(str + offset, MAXSTR - offset, "s");
} else if (interval_ms > 0) {
(void) snprintf(str + offset, MAXSTR - offset, "%ums", interval_ms);
} else if (str[0] == '\0') {
strcpy(str, "0s");
}
return str;
}
diff --git a/lib/common/strings.c b/lib/common/strings.c
index acf174d22d..1e5c2b2278 100644
--- a/lib/common/strings.c
+++ b/lib/common/strings.c
@@ -1,1442 +1,1443 @@
/*
* 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.
*/
#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;
}
/*!
* \brief Parse a time+units string and return milliseconds equivalent
*
* \param[in] input String with a nonnegative 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
* \c PCMK__PARSE_INT_DEFAULT on error
*/
long long
crm_get_msec(const char *input)
{
char *units = NULL; // Do not free; will point to part of input
long long multiplier = 1000;
long long divisor = 1;
long long msec = PCMK__PARSE_INT_DEFAULT;
if (input == NULL) {
return PCMK__PARSE_INT_DEFAULT;
}
// Skip initial whitespace
while (isspace(*input)) {
input++;
}
// Reject negative and unparsable inputs
scan_ll(input, &msec, -1, &units);
if (msec < 0) {
return PCMK__PARSE_INT_DEFAULT;
}
/* If the number is a decimal, scan_ll() reads only the integer part. Skip
* any remaining digits or decimal characters.
*
* @COMPAT Well-formed and malformed decimals are both accepted inputs. For
* example, "3.14 ms" and "3.1.4 ms" are treated the same as "3ms" and
* parsed successfully. At a compatibility break, decide if this is still
* desired.
*/
while (isdigit(*units) || (*units == '.')) {
units++;
}
// Skip any additional whitespace after the number
while (isspace(*units)) {
units++;
}
/* @COMPAT Use exact comparisons. Currently, we match too liberally, and the
* second strncasecmp() in each case is redundant.
*/
if ((*units == '\0')
|| (strncasecmp(units, "s", 1) == 0)
|| (strncasecmp(units, "sec", 3) == 0)) {
multiplier = 1000;
divisor = 1;
} else if ((strncasecmp(units, "ms", 2) == 0)
|| (strncasecmp(units, "msec", 4) == 0)) {
multiplier = 1;
divisor = 1;
} else if ((strncasecmp(units, "us", 2) == 0)
|| (strncasecmp(units, "usec", 4) == 0)) {
multiplier = 1;
divisor = 1000;
} else if ((strncasecmp(units, "m", 1) == 0)
|| (strncasecmp(units, "min", 3) == 0)) {
multiplier = 60 * 1000;
divisor = 1;
} else if ((strncasecmp(units, "h", 1) == 0)
|| (strncasecmp(units, "hr", 2) == 0)) {
multiplier = 60 * 60 * 1000;
divisor = 1;
} else {
// Invalid units
return PCMK__PARSE_INT_DEFAULT;
}
// Apply units, capping at LLONG_MAX
if (msec > (LLONG_MAX / multiplier)) {
return LLONG_MAX;
}
return (msec * multiplier) / divisor;
}
/*!
* \brief Parse milliseconds from a Pacemaker interval specification
*
* \param[in] input Pacemaker time interval specification (a bare number
* of seconds; a number with a unit, optionally with
* whitespace before and/or after the number; or an ISO
* 8601 duration)
* \param[out] result_ms Where to store milliseconds equivalent of \p input on
* success (limited to the range of an unsigned integer),
* or 0 if \p input is \c NULL or invalid
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok if
* \p input is valid or \c NULL, and \c EINVAL otherwise)
*/
int
pcmk_parse_interval_spec(const char *input, guint *result_ms)
{
long long msec = PCMK__PARSE_INT_DEFAULT;
int rc = pcmk_rc_ok;
if (input == NULL) {
msec = 0;
goto done;
}
if (input[0] == 'P') {
crm_time_t *period_s = crm_time_parse_duration(input);
if (period_s != NULL) {
- msec = 1000 * crm_time_get_seconds(period_s);
+ msec = crm_time_get_seconds(period_s);
+ msec = QB_MIN(msec, G_MAXUINT / 1000) * 1000;
crm_time_free(period_s);
}
} else {
msec = crm_get_msec(input);
}
if (msec == PCMK__PARSE_INT_DEFAULT) {
crm_warn("Using 0 instead of invalid interval specification '%s'",
input);
msec = 0;
rc = EINVAL;
}
done:
if (result_ms != NULL) {
*result_ms = (msec >= G_MAXUINT)? G_MAXUINT : (guint) msec;
}
return rc;
}
gboolean
crm_is_true(const char *s)
{
gboolean ret = FALSE;
return (crm_str_to_boolean(s, &ret) < 0)? FALSE : ret;
}
int
crm_str_to_boolean(const char *s, int *ret)
{
if (s == NULL) {
return -1;
}
if (pcmk__strcase_any_of(s, PCMK_VALUE_TRUE, "on", "yes", "y", "1", NULL)) {
if (ret != NULL) {
*ret = TRUE;
}
return 1;
}
if (pcmk__strcase_any_of(s, PCMK_VALUE_FALSE, "off", "no", "n", "0",
NULL)) {
if (ret != NULL) {
*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);
}
/*!
* \internal
* \brief Insert string copies into a hash table as key and value
*
* \param[in,out] table Hash table to add to
* \param[in] name String to add a copy of as key
* \param[in] value String to add a copy of as value
*
* \note This asserts on invalid arguments or memory allocation failure.
*/
void
pcmk__insert_dup(GHashTable *table, const char *name, const char *value)
{
CRM_ASSERT((table != NULL) && (name != NULL));
g_hash_table_insert(table, pcmk__str_copy(name), pcmk__str_copy(value));
}
/* 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) {
pcmk__insert_dup((GHashTable *) user_data,
(const char *) key, (const char *) 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 = pcmk__assert_alloc((size_t) max, sizeof(char));
*result_len = max;
rc = BZ2_bzBuffToBuffCompress(compressed, result_len, uncompressed, length,
CRM_BZ2_BLOCKS, 0, CRM_BZ2_WORK);
rc = pcmk__bzlib2rc(rc);
free(uncompressed);
if (rc != pcmk_rc_ok) {
crm_err("Compression of %d bytes failed: %s " CRM_XS " rc=%d",
length, pcmk_rc_str(rc), rc);
free(compressed);
return rc;
}
#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 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 Copy a string, 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] 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().
*/
char *
pcmk__str_copy_as(const char *file, const char *function, uint32_t line,
const char *str)
{
if (str != NULL) {
char *result = strdup(str);
if (result == NULL) {
crm_abort(file, function, line, "Out of memory", FALSE, TRUE);
crm_exit(CRM_EX_OSERR);
}
return result;
}
return NULL;
}
/*!
* \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);
*str = pcmk__str_copy(value);
}
}
/*!
* \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

File Metadata

Mime Type
text/x-diff
Expires
Mon, Apr 21, 6:09 PM (21 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1665103
Default Alt Text
(145 KB)

Event Timeline