Page Menu
Home
ClusterLabs Projects
Search
Configure Global Search
Log In
Files
F3686886
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
145 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Apr 21, 6:09 PM (23 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1665103
Default Alt Text
(145 KB)
Attached To
Mode
rP Pacemaker
Attached
Detach File
Event Timeline
Log In to Comment