diff --git a/cts/cli/regression.dates.exp b/cts/cli/regression.dates.exp index 58bb3c6862e..a3b4a9c07d4 100644 --- a/cts/cli/regression.dates.exp +++ b/cts/cli/regression.dates.exp @@ -29,32 +29,32 @@ iso8601: Invalid interval specified: 20191077T15/P1M =#=#=#= End test: Invalid period - [20191077T15/P1M] - Invalid parameter (2) =#=#=#= * Passed: iso8601 - Invalid period - [20191077T15/P1M] =#=#=#= Begin test: Invalid period - [2019-10-01T25:00:00Z/P1M] =#=#=#= -crm_time_parse_sec error: 25:00:00Z/P1M is not a valid ISO 8601 time specification because 25 is not a valid hour +parse_hms error: 25:00:00Z/P1M is not a valid ISO 8601 time specification because 25 is not a valid hour iso8601: Invalid interval specified: 2019-10-01T25:00:00Z/P1M =#=#=#= End test: Invalid period - [2019-10-01T25:00:00Z/P1M] - Invalid parameter (2) =#=#=#= * Passed: iso8601 - Invalid period - [2019-10-01T25:00:00Z/P1M] =#=#=#= Begin test: Invalid period - [2019-10-01T24:00:01Z/P1M] =#=#=#= -crm_time_parse_sec error: 24:00:01Z/P1M is not a valid ISO 8601 time specification because 24 is not a valid hour +parse_hms error: 24:00:01Z/P1M is not a valid ISO 8601 time specification because 24 is not a valid hour iso8601: Invalid interval specified: 2019-10-01T24:00:01Z/P1M =#=#=#= End test: Invalid period - [2019-10-01T24:00:01Z/P1M] - Invalid parameter (2) =#=#=#= * Passed: iso8601 - Invalid period - [2019-10-01T24:00:01Z/P1M] =#=#=#= Begin test: Invalid period - [PT5H/20191001T007000Z] =#=#=#= -crm_time_parse_sec error: 007000Z is not a valid ISO 8601 time specification because 70 is not a valid minute +parse_hms error: 007000Z is not a valid ISO 8601 time specification because 70 is not a valid minute iso8601: Invalid interval specified: PT5H/20191001T007000Z =#=#=#= End test: Invalid period - [PT5H/20191001T007000Z] - Invalid parameter (2) =#=#=#= * Passed: iso8601 - Invalid period - [PT5H/20191001T007000Z] =#=#=#= Begin test: Invalid period - [2019-10-01 00:00:80Z/P1M] =#=#=#= -crm_time_parse_sec error: 00:00:80Z/P1M is not a valid ISO 8601 time specification because 80 is not a valid second +parse_hms error: 00:00:80Z/P1M is not a valid ISO 8601 time specification because 80 is not a valid second iso8601: Invalid interval specified: 2019-10-01 00:00:80Z/P1M =#=#=#= End test: Invalid period - [2019-10-01 00:00:80Z/P1M] - Invalid parameter (2) =#=#=#= * Passed: iso8601 - Invalid period - [2019-10-01 00:00:80Z/P1M] =#=#=#= Begin test: Invalid period - [2019-10-01 00:00:10 +25:00/P1M] =#=#=#= -crm_time_parse_sec error: 25:00/P1M is not a valid ISO 8601 time specification because 25 is not a valid hour +parse_hms error: 25:00/P1M is not a valid ISO 8601 time specification because 25 is not a valid hour iso8601: Invalid interval specified: 2019-10-01 00:00:10 +25:00/P1M =#=#=#= End test: Invalid period - [2019-10-01 00:00:10 +25:00/P1M] - Invalid parameter (2) =#=#=#= * Passed: iso8601 - Invalid period - [2019-10-01 00:00:10 +25:00/P1M] =#=#=#= Begin test: Invalid period - [20191001T000010 -00:61/P1M] =#=#=#= -crm_time_parse_sec error: 00:61/P1M is not a valid ISO 8601 time specification because 61 is not a valid minute +parse_hms error: 00:61/P1M is not a valid ISO 8601 time specification because 61 is not a valid minute iso8601: Invalid interval specified: 20191001T000010 -00:61/P1M =#=#=#= End test: Invalid period - [20191001T000010 -00:61/P1M] - Invalid parameter (2) =#=#=#= * Passed: iso8601 - Invalid period - [20191001T000010 -00:61/P1M] diff --git a/cts/cts-cli.in b/cts/cts-cli.in index b54bcdd4b23..fe6786cb835 100644 --- a/cts/cts-cli.in +++ b/cts/cts-cli.in @@ -253,7 +253,7 @@ def sanitize_output(s): (r'@crm_feature_set=[0-9.]+, ', r''), (r'\(crm_time_parse_duration@.*\.c:[0-9]+\)', r'crm_time_parse_duration'), (r'\(crm_time_parse_period@.*\.c:[0-9]+\)', r'crm_time_parse_period'), - (r'\(crm_time_parse_sec@.*\.c:[0-9]+\)', r'crm_time_parse_sec'), + (r'\(parse_hms@.*\.c:[0-9]+\)', r'parse_hms'), (re.escape(cts_cli_data), r'CTS_CLI_DATA'), (r' default="[^"]*"', r' default=""'), (r' end="[0-9][-+: 0-9]*Z*"', r' end=""'), diff --git a/include/crm/common/Makefile.am b/include/crm/common/Makefile.am index 25c23ccd2c1..09a57b8b902 100644 --- a/include/crm/common/Makefile.am +++ b/include/crm/common/Makefile.am @@ -23,6 +23,7 @@ header_HEADERS = acl.h \ ipc_pacemakerd.h \ ipc_schedulerd.h \ iso8601.h \ + iso8601_compat.h \ logging.h \ logging_compat.h \ mainloop.h \ diff --git a/include/crm/common/acl.h b/include/crm/common/acl.h index 0933a4554c6..bff52f63417 100644 --- a/include/crm/common/acl.h +++ b/include/crm/common/acl.h @@ -30,12 +30,12 @@ bool xml_acl_filtered_copy(const char *user, xmlNode* acl_source, xmlNode *xml, bool pcmk_acl_required(const char *user); -#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) -#include -#endif // !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) - #ifdef __cplusplus } #endif +#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) +#include +#endif // !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) + #endif // PCMK__CRM_COMMON_ACL__H diff --git a/include/crm/common/iso8601.h b/include/crm/common/iso8601.h index 4c6addaf8e3..9f26bd86f39 100644 --- a/include/crm/common/iso8601.h +++ b/include/crm/common/iso8601.h @@ -1,5 +1,5 @@ /* - * Copyright 2005-2024 the Pacemaker project contributors + * Copyright 2005-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -29,6 +29,12 @@ extern "C" { * See https://en.wikipedia.org/wiki/ISO_8601 */ +/*! + * \brief An opaque date and time object + * + * \note Negative years are treated inconsistently and should not be relied + * upon. + */ typedef struct crm_time_s crm_time_t; typedef struct crm_time_period_s { @@ -57,13 +63,6 @@ void crm_time_free(crm_time_t * dt); bool crm_time_is_defined(const crm_time_t *t); char *crm_time_as_string(const crm_time_t *dt, int flags); -#define crm_time_log(level, prefix, dt, flags) \ - crm_time_log_alias(level, __FILE__, __func__, __LINE__, prefix, dt, flags) - -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); - #define crm_time_log_date 0x001 #define crm_time_log_timeofday 0x002 #define crm_time_log_with_timezone 0x004 @@ -85,12 +84,9 @@ int crm_time_compare(const crm_time_t *a, const crm_time_t *b); int crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m, uint32_t *s); -int crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m); int crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m, uint32_t *d); int crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d); -int crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w, - uint32_t * d); /* Time in seconds since 0000-01-01 00:00:00Z */ long long crm_time_get_seconds(const crm_time_t *dt); @@ -98,9 +94,6 @@ long long crm_time_get_seconds(const crm_time_t *dt); /* Time in seconds since 1970-01-01 00:00:00Z */ long long crm_time_get_seconds_since_epoch(const crm_time_t *dt); -void crm_time_set(crm_time_t *target, const crm_time_t *source); -void crm_time_set_timet(crm_time_t *target, const time_t *source); - /* Returns a new time object */ crm_time_t *pcmk_copy_time(const crm_time_t *source); crm_time_t *crm_time_add(const crm_time_t *dt, const crm_time_t *value); @@ -115,16 +108,12 @@ void crm_time_add_weeks(crm_time_t * dt, int value); void crm_time_add_months(crm_time_t * dt, int value); void crm_time_add_years(crm_time_t * dt, int value); -/* Useful helper functions */ -int crm_time_january1_weekday(int year); -int crm_time_weeks_in_year(int year); -int crm_time_days_in_month(int month, int year); - -bool crm_time_leapyear(int year); -bool crm_time_check(const crm_time_t *dt); - #ifdef __cplusplus } #endif +#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) +#include +#endif + #endif diff --git a/include/crm/common/iso8601_compat.h b/include/crm/common/iso8601_compat.h new file mode 100644 index 00000000000..69d551ee451 --- /dev/null +++ b/include/crm/common/iso8601_compat.h @@ -0,0 +1,73 @@ +/* + * Copyright 2004-2025 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef PCMK__CRM_COMMON_ISO8601_COMPAT__H +#define PCMK__CRM_COMMON_ISO8601_COMPAT__H + +#include +#include // uint32_t +#include // time_t + +#include // crm_time_t + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \file + * \brief Deprecated Pacemaker time API + * \ingroup core + * \deprecated Do not include this header directly. The time APIs in this + * header, and the header itself, will be removed in a future + * release. + */ + +//! \deprecated Do not use +bool crm_time_leapyear(int year); + +//! \deprecated Do not use +int crm_time_days_in_month(int month, int year); + +//! \deprecated Do not use +int crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m); + +//! \deprecated Do not use +int crm_time_weeks_in_year(int year); + +//! \deprecated Do not use +int crm_time_january1_weekday(int year); + +//! \deprecated Do not use +void crm_time_set(crm_time_t *target, const crm_time_t *source); + +//! \deprecated Do not use +bool crm_time_check(const crm_time_t *dt); + +//! \deprecated Do not use +void crm_time_set_timet(crm_time_t *target, const time_t *source_sec); + +//! \deprecated Do not use +int crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w, + uint32_t *d); + +//! \deprecated Do not use +#define crm_time_log(level, prefix, dt, flags) \ + crm_time_log_alias(level, __FILE__, __func__, __LINE__, prefix, dt, flags) + +//! \deprecated Do not use +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); + +#ifdef __cplusplus +} +#endif + +#endif // PCMK__CRM_COMMON_ISO8601_COMPAT__H diff --git a/include/crm/common/iso8601_internal.h b/include/crm/common/iso8601_internal.h index 54f89f008ea..fad5060f3fb 100644 --- a/include/crm/common/iso8601_internal.h +++ b/include/crm/common/iso8601_internal.h @@ -10,21 +10,32 @@ #ifndef PCMK__CRM_COMMON_ISO8601_INTERNAL__H #define PCMK__CRM_COMMON_ISO8601_INTERNAL__H -#include -#include -#include #include +#include // uint8_t, uint32_t +#include +#include + +#include #include #ifdef __cplusplus extern "C" { #endif +void pcmk__time_get_ywd(const crm_time_t *dt, uint32_t *y, uint32_t *w, + uint32_t *d); char *pcmk__time_format_hr(const char *format, const crm_time_t *dt, int usec); char *pcmk__epoch2str(const time_t *source, uint32_t flags); char *pcmk__timespec2str(const struct timespec *ts, uint32_t flags); const char *pcmk__readable_interval(guint interval_ms); -crm_time_t *pcmk__copy_timet(time_t source); +crm_time_t *pcmk__copy_timet(time_t source_sec); + +void pcmk__time_log_as(const char *file, const char *function, int line, + uint8_t level, const char *prefix, const crm_time_t *dt, + uint32_t flags); + +#define pcmk__time_log(level, prefix, dt, flags) \ + pcmk__time_log_as(__FILE__, __func__, __LINE__, level, prefix, dt, flags); // A date/time or duration struct crm_time_s { diff --git a/include/crm/common/strings.h b/include/crm/common/strings.h index eb753d1f093..c462b2c294b 100644 --- a/include/crm/common/strings.h +++ b/include/crm/common/strings.h @@ -24,12 +24,12 @@ extern "C" { int pcmk_parse_interval_spec(const char *input, guint *result_ms); -#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) -#include -#endif - #ifdef __cplusplus } #endif +#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) +#include +#endif + #endif // PCMK__CRM_COMMON_STRINGS__H diff --git a/include/crm/common/xml_io.h b/include/crm/common/xml_io.h index c8dbfd06086..aa76b49175f 100644 --- a/include/crm/common/xml_io.h +++ b/include/crm/common/xml_io.h @@ -20,12 +20,12 @@ extern "C" { * \ingroup core */ -#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) -#include -#endif - #ifdef __cplusplus } #endif +#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) +#include +#endif + #endif // PCMK__CRM_COMMON_XML_IO__H diff --git a/lib/common/fuzzers/iso8601_fuzzer.c b/lib/common/fuzzers/iso8601_fuzzer.c index 65a2d430cea..a600dfd57b1 100644 --- a/lib/common/fuzzers/iso8601_fuzzer.c +++ b/lib/common/fuzzers/iso8601_fuzzer.c @@ -24,7 +24,7 @@ LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) crm_time_period_t *period = NULL; struct timespec tv = { 0, }; - crm_time_t now = { 0, }; + crm_time_t *now = NULL; char *result = NULL; // Ensure we have enough data. @@ -38,9 +38,10 @@ LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) crm_time_free_period(period); qb_util_timespec_from_epoch_get(&tv); - crm_time_set_timet(&now, &(tv.tv_sec)); - result = pcmk__time_format_hr(ns, &now, + now = pcmk__copy_timet(tv.tv_sec); + result = pcmk__time_format_hr(ns, now, (int) (tv.tv_nsec / QB_TIME_NS_IN_USEC)); + crm_time_free(now); free(result); free(ns); diff --git a/lib/common/iso8601.c b/lib/common/iso8601.c index 48964e599dd..91f2c03c31a 100644 --- a/lib/common/iso8601.c +++ b/lib/common/iso8601.c @@ -50,8 +50,11 @@ # define GMTOFF(tm) (-timezone+daylight) #endif -#define HOUR_SECONDS (60 * 60) -#define DAY_SECONDS (HOUR_SECONDS * 24) +#define SECONDS_IN_MINUTE 60 +#define MINUTES_IN_HOUR 60 +#define SECONDS_IN_HOUR (SECONDS_IN_MINUTE * MINUTES_IN_HOUR) +#define HOURS_IN_DAY 24 +#define SECONDS_IN_DAY (SECONDS_IN_HOUR * HOURS_IN_DAY) /*! * \internal @@ -69,89 +72,82 @@ ((QB_ABS(usec) < QB_TIME_US_IN_SEC) \ && (((sec) == 0) || ((usec) == 0) || (((sec) < 0) == ((usec) < 0)))) -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 + * \return Newly allocated time object (guaranteed not to be \c NULL) + * * \note The caller is responsible for freeing the return value using - * crm_time_free(). + * \c crm_time_free(). */ crm_time_t * crm_time_new_undefined(void) { - return (crm_time_t *) pcmk__assert_alloc(1, sizeof(crm_time_t)); + return pcmk__assert_alloc(1, sizeof(crm_time_t)); +} + +static bool +is_leap_year(int year) +{ + return ((year % 4) == 0) + && (((year % 100) != 0) || (year % 400 == 0)); } +// 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 Check whether a time object has been initialized yet + * \brief Return number of days in given month of given year * - * \param[in] t Time object to check + * \param[in] month Ordinal month (1-12) + * \param[in] year Gregorian year * - * \return \c true if time object has been initialized, \c false otherwise + * \return Number of days in given month (0 if given month or year is invalid) */ -bool -crm_time_is_defined(const crm_time_t *t) +static int +days_in_month_year(int month, int year) { - // 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); + if ((month < 1) || (month > 12) || (year < 1)) { + return 0; + } + if ((month == 2) && is_leap_year(year)) { + month = 13; + } + return month_days[month - 1]; } -void -crm_time_free(crm_time_t * dt) +/*! + * \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) { - if (dt == NULL) { - return; + int result = 0; + + 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 += days_in_month_year(lpc, y); } - free(dt); + return result; } static int year_days(int year) { - return crm_time_leapyear(year)? 366 : 365; + return is_leap_year(year)? 366 : 365; } /* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt : @@ -162,8 +158,8 @@ year_days(int year) * G = YY + YY/4 * Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7) */ -int -crm_time_january1_weekday(int year) +static int +jan1_day_of_week(int year) { int YY = (year - 1) % 100; int C = (year - 1) - YY; @@ -175,17 +171,17 @@ crm_time_january1_weekday(int year) return jan1; } -int -crm_time_weeks_in_year(int year) +static int +weeks_in_year(int year) { int weeks = 52; - int jan1 = crm_time_january1_weekday(year); + int jan1 = jan1_day_of_week(year); /* if jan1 == thursday */ if (jan1 == 4) { weeks++; } else { - jan1 = crm_time_january1_weekday(year + 1); + jan1 = jan1_day_of_week(year + 1); /* if dec31 == thursday aka. jan1 of next year is a friday */ if (jan1 == 5) { weeks++; @@ -195,868 +191,871 @@ crm_time_weeks_in_year(int year) 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 + * \internal + * \brief Determine number of seconds from an hour:minute:second string * - * \param[in] month Ordinal month (1-12) - * \param[in] year Gregorian year + * \param[in] time_str Time specification string + * \param[out] result Number of seconds equivalent to time_str * - * \return Number of days in given month (0 if given month or year is invalid) + * \return \c true if specification was valid, or \c false 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. */ -int -crm_time_days_in_month(int month, int year) +static bool +parse_hms(const char *time_str, int *result) { - if ((month < 1) || (month > 12) || (year < 1)) { - return 0; + 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 ((month == 2) && crm_time_leapyear(year)) { - month = 13; + if (rc == 0) { + crm_err("%s is not a valid ISO 8601 time specification", time_str); + return false; } - return month_days[month - 1]; -} -bool -crm_time_leapyear(int year) -{ - bool is_leap = false; + crm_trace("Got valid time: %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, + hour, minute, second); - if (year % 4 == 0) { - is_leap = true; + if ((hour == HOURS_IN_DAY) && (minute == 0) && (second == 0)) { + // Equivalent to 00:00:00 of next day, return number of seconds in day + } else if (hour >= HOURS_IN_DAY) { + crm_err("%s is not a valid ISO 8601 time specification " + "because %" PRIu32 " is not a valid hour", time_str, hour); + return false; + } + if (minute >= MINUTES_IN_HOUR) { + crm_err("%s is not a valid ISO 8601 time specification " + "because %" PRIu32 " is not a valid minute", time_str, minute); + return false; } - if (year % 100 == 0 && year % 400 != 0) { - is_leap = false; + if (second >= SECONDS_IN_MINUTE) { + crm_err("%s is not a valid ISO 8601 time specification " + "because %" PRIu32 " is not a valid second", time_str, second); + return false; } - return is_leap; + + *result = (hour * SECONDS_IN_HOUR) + (minute * SECONDS_IN_MINUTE) + second; + return true; } -/*! - * \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) +static bool +parse_offset(const char *offset_str, int *offset) { - int result = 0; + tzset(); - CRM_CHECK((y > 0) && (y <= INT_MAX) && (m >= 1) && (m <= 12) - && (d >= 1) && (d <= 31), return 0); + 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) / SECONDS_IN_HOUR; + int m_offset = (GMTOFF(now_tm) - (SECONDS_IN_HOUR * h_offset)) + / SECONDS_IN_MINUTE; - result = d; - for (int lpc = 1; lpc < m; lpc++) { - result += crm_time_days_in_month(lpc, y); + if (h_offset < 0 && m_offset < 0) { + m_offset = 0 - m_offset; + } + *offset = (SECONDS_IN_HOUR * h_offset) + (SECONDS_IN_MINUTE * m_offset); + return true; } - 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); + if (offset_str[0] == 'Z') { // @TODO invalid if anything after? + *offset = 0; + return true; } - 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; + *offset = 0; + if ((offset_str[0] == '+') || (offset_str[0] == '-') + || isdigit((int)offset_str[0])) { - crm_trace("%d == %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, - sec, hours, minutes, seconds); + bool negate = false; - *h = hours; - *m = minutes; - *s = seconds; + if (offset_str[0] == '+') { + offset_str++; + } else if (offset_str[0] == '-') { + negate = true; + offset_str++; + } + if (!parse_hms(offset_str, offset)) { + return false; + } + if (negate) { + *offset = 0 - *offset; + } + } // @TODO else invalid? + return true; } -int -crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m, - uint32_t *s) +static void +seconds_to_hms(int seconds, uint32_t *h, uint32_t *m, uint32_t *s) { - crm_time_get_sec(dt->seconds, h, m, s); - return TRUE; -} + int hours = 0; + int minutes = 0; -int -crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m) -{ - uint32_t s; + hours = seconds / SECONDS_IN_HOUR; + seconds %= SECONDS_IN_HOUR; - crm_time_get_sec(dt->seconds, h, m, &s); - return TRUE; + minutes = seconds / SECONDS_IN_MINUTE; + seconds %= SECONDS_IN_MINUTE; + + *h = (uint32_t) QB_ABS(hours); + *m = (uint32_t) QB_ABS(minutes); + *s = (uint32_t) QB_ABS(seconds); } -long long -crm_time_get_seconds(const crm_time_t *dt) +static bool +parse_time(const char *time_str, crm_time_t *a_time) { - int lpc; - crm_time_t *utc = NULL; - long long in_seconds = 0; + uint32_t h, m, s; + char *offset_s = NULL; - if (dt == NULL) { - return 0; - } + tzset(); - // @TODO This is inefficient if dt is already in UTC - utc = crm_get_utc_time(dt); - if (utc == NULL) { - return 0; - } + if (time_str != NULL) { + if (!parse_hms(time_str, &(a_time->seconds))) { + return false; + } - // @TODO We should probably use <= if dt is a duration - for (lpc = 1; lpc < utc->years; lpc++) { - long long dmax = year_days(lpc); + offset_s = strstr(time_str, "Z"); - in_seconds += DAY_SECONDS * dmax; - } + /* @COMPAT: Spaces between the time and the offset are not supported + * by the standard according to section 3.4.1 and 4.2.5.2. + */ + if (offset_s == NULL) { + offset_s = strpbrk(time_str, " +-"); + } - /* utc->months can be set only for durations. By definition, the value - * varies depending on the (unknown) start date to which the duration will - * be applied. Assume 30-day months so that something vaguely sane happens - * in this case. - */ - if (utc->months > 0) { - in_seconds += DAY_SECONDS * 30 * (long long) (utc->months); + if (offset_s != NULL) { + while (isspace(*offset_s)) { + offset_s++; + } + } } - if (utc->days > 0) { - in_seconds += DAY_SECONDS * (long long) (utc->days - 1); + if (!parse_offset(offset_s, &(a_time->offset))) { + return false; } - in_seconds += utc->seconds; - crm_time_free(utc); - return in_seconds; + seconds_to_hms(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 == SECONDS_IN_DAY) { + // 24:00:00 == 00:00:00 of next day + a_time->seconds = 0; + crm_time_add_days(a_time, 1); + } + return true; } -#define EPOCH_SECONDS 62135596800ULL /* Calculated using crm_time_get_seconds() */ -long long -crm_time_get_seconds_since_epoch(const crm_time_t *dt) +/*! + * \internal + * \brief Check whether a time object represents a sensible date/time + * + * \param[in] dt Date/time object to check + * + * \return \c true if days and seconds are valid given the year, or \c false + * otherwise + */ +static bool +valid_time(const crm_time_t *dt) { - return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS); + return (dt != NULL) + && (dt->days > 0) && (dt->days <= year_days(dt->years)) + && (dt->seconds >= 0) && (dt->seconds < SECONDS_IN_DAY); } -int -crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m, - uint32_t *d) +/* + * \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) { - int months = 0; - int days = dt->days; + const uint32_t flags = crm_time_log_date|crm_time_log_timeofday; + const char *time_s = NULL; + crm_time_t *dt = NULL; - if(dt->years != 0) { - for (months = 1; months <= 12 && days > 0; months++) { - int mdays = crm_time_days_in_month(months, dt->years); + uint32_t year = 0U; + uint32_t month = 0U; + uint32_t day = 0U; + uint32_t week = 0U; - if (mdays >= days) { - break; - } else { - days -= mdays; - } + 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 = pcmk__copy_timet(time(NULL)); + if (date_str[0] == 'T') { + time_s = date_str + 1; + } else { + time_s = date_str; } + goto parse_time_segment; + } - } else if (dt->months) { - /* This is a duration including months, don't convert the days field */ - months = dt->months; + dt = crm_time_new_undefined(); - } else { - /* This is a duration not including months, still don't convert the days field */ + 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; + pcmk__time_log(LOG_TRACE, "Unpacked", dt, flags); + return dt; } - *y = dt->years; - *m = months; - *d = days; - crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days); - return TRUE; -} + /* YYYY-MM-DD */ + rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32 "-%" SCNu32 "", + &year, &month, &day); + if (rc == 1) { + /* YYYYMMDD */ + rc = sscanf(date_str, "%4" SCNu32 "%2" SCNu32 "%2" SCNu32 "", + &year, &month, &day); + } + if (rc == 3) { + if ((month < 1U) || (month > 12U)) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%" PRIu32 "' is not a valid month", + date_str, month); + goto invalid; + } else 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 < 1) || (day > INT_MAX) + || (day > days_in_month_year(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 '%.4" PRIu32 "-%.3d' " + "from date string '%s'", year, dt->days, date_str); + } + goto parse_time_segment; + } -int -crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d) -{ - *y = dt->years; - *d = dt->days; - return TRUE; -} + /* YYYY-DDD */ + rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32, &year, &day); + if (rc == 2) { + 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 '%" 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_segment; + } -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; + /* YYYY-Www-D */ + rc = sscanf(date_str, "%" SCNu32 "-W%" SCNu32 "-%" SCNu32, + &year, &week, &day); + if (rc == 3) { + if ((week < 1U) || (week > weeks_in_year(year))) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%" PRIu32 "' is not a valid week of year %" + PRIu32 " (1-%d)", + date_str, week, year, weeks_in_year(year)); + goto invalid; + } else if ((day < 1U) || (day > 7U)) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "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 1. If 1 January is on a Friday, Saturday or Sunday, + * it is in week 52 or 53 of the previous year. + */ + int jan1 = jan1_day_of_week(year); - CRM_CHECK(dt->days > 0, return FALSE); + crm_trace("Parsed year %" PRIu32 " (Jan 1 = %d), week %" PRIu32 + ", and day %" PRIu32 " from date string '%s'", + year, jan1, week, day, date_str); -/* 6. Find the Weekday for Y M D */ - h = dt->days + jan1 - 1; - *d = 1 + ((h - 1) % 7); + dt->years = year; + crm_time_add_days(dt, (week - 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); + if (jan1 <= 4) { + crm_time_add_days(dt, 1 - jan1); + } else { + crm_time_add_days(dt, 8 - jan1); + } - } else { - year_num = dt->years; + crm_time_add_days(dt, day); + } + goto parse_time_segment; } -/* 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; + crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str); + goto invalid; - 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; +parse_time_segment: + 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) && !parse_time(time_s, dt)) { + goto invalid; + } -/* 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); + pcmk__time_log(LOG_TRACE, "Unpacked", dt, flags); - *w = j / 7; - if (jan1 > 4) { - *w -= 1; - } + if (!valid_time(dt)) { + crm_err("'%s' is not a valid ISO 8601 date/time specification", + date_str); + goto invalid; } + return dt; - *y = year_num; - crm_trace("Converted %.4d-%.3d to %.4" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, - dt->years, dt->days, *y, *w, *d); - return TRUE; +invalid: + crm_time_free(dt); + errno = EINVAL; + return NULL; } -#define DATE_MAX 128 - /*! * \internal - * \brief Print "." to a buffer + * \brief Parse the time portion of an ISO 8601 date/time string * - * \param[in] sec Seconds - * \param[in] usec Microseconds (must be of same sign as \p sec and of - * absolute value less than \c QB_TIME_US_IN_SEC) - * \param[in,out] buf Result buffer + * \param[in] time_str Time portion of specification (after any 'T') + * \param[in,out] a_time Time object to parse into + * + * \return \c true if valid time was parsed, \c false otherwise + * \note This may add a day to a_time (if the time is 24:00:00). */ -static inline void -sec_usec_as_string(long long sec, int usec, GString *buf) +// Return value is guaranteed not to be NULL +static crm_time_t * +utc_from_crm_time(const crm_time_t *dt) { - /* A negative value smaller than -1 second should have the negative sign - * before the 0, not before the usec part - */ - if ((sec == 0) && (usec < 0)) { - g_string_append_c(buf, '-'); + const uint32_t flags = crm_time_log_date + |crm_time_log_timeofday + |crm_time_log_with_timezone; + crm_time_t *utc = NULL; + + pcmk__assert(dt != 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; } - g_string_append_printf(buf, "%lld.%06d", sec, QB_ABS(usec)); + + pcmk__time_log(LOG_TRACE, "utc-source", dt, flags); + pcmk__time_log(LOG_TRACE, "utc-target", utc, flags); + 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); } /*! - * \internal - * \brief Get a string representation of a duration + * \brief Check whether a time object has been initialized yet * - * \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 buf - * \param[in,out] buf Result buffer + * \param[in] t Time object to check + * + * \return \c true if time object has been initialized, \c false otherwise */ -static void -duration_as_string(const crm_time_t *dt, int usec, bool show_usec, GString *buf) +bool +crm_time_is_defined(const crm_time_t *t) { - pcmk__assert(valid_sec_usec(dt->seconds, usec)); - - if (dt->years) { - g_string_append_printf(buf, "%4d year%s ", - dt->years, pcmk__plural_s(dt->years)); - } - if (dt->months) { - g_string_append_printf(buf, "%2d month%s ", - dt->months, pcmk__plural_s(dt->months)); - } - if (dt->days) { - g_string_append_printf(buf, "%2d day%s ", - dt->days, pcmk__plural_s(dt->days)); - } + // 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); +} - // At least print seconds (and optionally usecs) - if ((buf->len == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) { - if (show_usec) { - sec_usec_as_string(dt->seconds, usec, buf); - } else { - g_string_append_printf(buf, "%d", dt->seconds); - } - g_string_append_printf(buf, " second%s", pcmk__plural_s(dt->seconds)); +void +crm_time_free(crm_time_t * dt) +{ + if (dt == NULL) { + return; } + free(dt); +} - // 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))); - - g_string_append(buf, " ("); - - if (h) { - g_string_append_printf(buf, "%" PRIu32 " hour%s", - h, pcmk__plural_s(h)); - - if ((m != 0) || print_sec_component) { - g_string_append_c(buf, ' '); - } - } - - if (m) { - g_string_append_printf(buf, "%" PRIu32 " minute%s", - m, pcmk__plural_s(m)); +void +pcmk__time_log_as(const char *file, const char *function, int line, + uint8_t level, const char *prefix, const crm_time_t *dt, + uint32_t flags) +{ + char *date_s = crm_time_as_string(dt, flags); - if (print_sec_component) { - g_string_append_c(buf, ' '); - } - } + if (prefix != NULL) { + char *old = date_s; - if (print_sec_component) { - if (show_usec) { - sec_usec_as_string(s, u, buf); - } else { - g_string_append_printf(buf, "%" PRIu32, s); - } - g_string_append_printf(buf, " second%s", - pcmk__plural_s(dt->seconds)); - } + date_s = pcmk__assert_asprintf("%s: %s", prefix, date_s); + free(old); + } - g_string_append_c(buf, ')'); + if (level == LOG_STDOUT) { + printf("%s\n", date_s); + } else { + do_crm_log_alias(level, file, function, line, "%s", date_s); } + free(date_s); } -/*! - * \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 \c crm_time_* string format options - * - * \return Newly allocated string representation of \p dt plus \p usec - * - * \note The caller is responsible for freeing the return value using \c free(). - */ -static char * -time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags) +int +crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m, + uint32_t *s) { + seconds_to_hms(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; - GString *buf = NULL; - char *result = NULL; + long long in_seconds = 0; - if (!crm_time_is_defined(dt)) { - return pcmk__str_copy(""); + if (dt == NULL) { + return 0; } - pcmk__assert(valid_sec_usec(dt->seconds, usec)); + // @TODO This is inefficient if dt is already in UTC + utc = utc_from_crm_time(dt); + if (utc == NULL) { + return 0; + } - buf = g_string_sized_new(128); + // @TODO We should probably use <= if dt is a duration + for (lpc = 1; lpc < utc->years; lpc++) { + long long dmax = year_days(lpc); - /* Simple cases: as duration, seconds, or seconds since epoch. - * These never depend on time zone. + in_seconds += SECONDS_IN_DAY * dmax; + } + + /* utc->months can be set only for durations. By definition, the value + * varies depending on the (unknown) start date to which the duration will + * be applied. Assume 30-day months so that something vaguely sane happens + * in this case. */ + if (utc->months > 0) { + in_seconds += SECONDS_IN_DAY * 30 * (long long) (utc->months); + } - if (pcmk__is_set(flags, crm_time_log_duration)) { - duration_as_string(dt, usec, pcmk__is_set(flags, crm_time_usecs), buf); - goto done; + if (utc->days > 0) { + in_seconds += SECONDS_IN_DAY * (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 (pcmk__any_flags_set(flags, crm_time_seconds|crm_time_epoch)) { - long long seconds = 0; + if(dt->years != 0) { + for (months = 1; months <= 12 && days > 0; months++) { + int mdays = days_in_month_year(months, dt->years); - if (pcmk__is_set(flags, crm_time_seconds)) { - seconds = crm_time_get_seconds(dt); - } else { - seconds = crm_time_get_seconds_since_epoch(dt); + if (mdays >= days) { + break; + } else { + days -= mdays; + } } - if (pcmk__is_set(flags, crm_time_usecs)) { - sec_usec_as_string(seconds, usec, buf); - } else { - g_string_append_printf(buf, "%lld", seconds); - } - goto done; - } + } else if (dt->months) { + /* This is a duration including months, don't convert the days field */ + months = dt->months; - // 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; + } else { + /* This is a duration not including months, still don't convert the days field */ } - // As readable string + *y = dt->years; + *m = months; + *d = days; + crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days); + return TRUE; +} - 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; +int +crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d) +{ + *y = dt->years; + *d = dt->days; + return TRUE; +} - if (crm_time_get_isoweek(dt, &y, &w, &d)) { - g_string_append_printf(buf, - "%" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, - y, w, d); - } +void +pcmk__time_get_ywd(const crm_time_t *dt, uint32_t *y, uint32_t *w, uint32_t *d) +{ + // Based on ISO week date: https://en.wikipedia.org/wiki/ISO_week_date + int year_num = 0; + int jan1 = jan1_day_of_week(dt->years); + int h = -1; - } else if (pcmk__is_set(flags, crm_time_ordinal)) { // YYYY-DDD - uint32_t y = 0; - uint32_t d = 0; + if (dt->days <= 0) { + return; + } - if (crm_time_get_ordinal(dt, &y, &d)) { - g_string_append_printf(buf, "%" PRIu32 "-%.3" PRIu32, y, d); - } +/* 6. Find the Weekday for Y M D */ + h = dt->days + jan1 - 1; + *d = 1 + ((h - 1) % 7); - } else { // YYYY-MM-DD - uint32_t y = 0; - uint32_t m = 0; - uint32_t d = 0; +/* 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 = weeks_in_year(year_num); - if (crm_time_get_gregorian(dt, &y, &m, &d)) { - g_string_append_printf(buf, - "%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, - y, m, d); - } - } + } else { + year_num = dt->years; } - if (pcmk__is_set(flags, crm_time_log_timeofday)) { - uint32_t h = 0, m = 0, s = 0; - - if (buf->len > 0) { - g_string_append_c(buf, ' '); - } - - if (crm_time_get_timeofday(dt, &h, &m, &s)) { - g_string_append_printf(buf, - "%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, - h, m, s); +/* 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 (pcmk__is_set(flags, crm_time_usecs)) { - g_string_append_printf(buf, ".%06" PRIu32, QB_ABS(usec)); - } + 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; } + } - if (pcmk__is_set(flags, crm_time_log_with_timezone) - && (dt->offset != 0)) { - crm_time_get_sec(dt->offset, &h, &m, &s); - g_string_append_printf(buf, " %c%.2" PRIu32 ":%.2" PRIu32, - ((dt->offset < 0)? '-' : '+'), h, m); +/* 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); - } else { - g_string_append_c(buf, 'Z'); + *w = j / 7; + if (jan1 > 4) { + *w -= 1; } } -done: - crm_time_free(utc); - result = pcmk__str_copy(buf->str); - g_string_free(buf, TRUE); - return result; + *y = year_num; + crm_trace("Converted %.4d-%.3d to %.4" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, + dt->years, dt->days, *y, *w, *d); } /*! - * \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 + * \internal + * \brief Print "." to a buffer * - * \note The caller is responsible for freeing the return value using \p free(). + * \param[in] sec Seconds + * \param[in] usec Microseconds (must be of same sign as \p sec and of + * absolute value less than \c QB_TIME_US_IN_SEC) + * \param[in,out] buf Result buffer */ -char * -crm_time_as_string(const crm_time_t *dt, int flags) +static inline void +sec_usec_as_string(long long sec, int usec, GString *buf) { - return time_as_string_common(dt, 0, flags); + /* A negative value smaller than -1 second should have the negative sign + * before the 0, not before the usec part + */ + if ((sec == 0) && (usec < 0)) { + g_string_append_c(buf, '-'); + } + g_string_append_printf(buf, "%lld.%06d", sec, QB_ABS(usec)); } /*! * \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 + * \brief Get a string representation of a duration * - * \return \c true if specification was valid, \c 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. + * \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 buf + * \param[in,out] buf Result buffer */ -static bool -crm_time_parse_sec(const char *time_str, int *result) +static void +duration_as_string(const crm_time_t *dt, int usec, bool show_usec, GString *buf) { - 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); + pcmk__assert(valid_sec_usec(dt->seconds, usec)); - 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; + if (dt->years) { + g_string_append_printf(buf, "%4d year%s ", + dt->years, pcmk__plural_s(dt->years)); } - - *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 (dt->months) { + g_string_append_printf(buf, "%2d month%s ", + dt->months, pcmk__plural_s(dt->months)); } - - if (offset_str[0] == 'Z') { // @TODO invalid if anything after? - *offset = 0; - return true; + if (dt->days) { + g_string_append_printf(buf, "%2d day%s ", + dt->days, pcmk__plural_s(dt->days)); } - *offset = 0; - if ((offset_str[0] == '+') || (offset_str[0] == '-') - || isdigit((int)offset_str[0])) { - - bool 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)) { - return false; - } - if (negate) { - *offset = 0 - *offset; + // At least print seconds (and optionally usecs) + if ((buf->len == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) { + if (show_usec) { + sec_usec_as_string(dt->seconds, usec, buf); + } else { + g_string_append_printf(buf, "%d", dt->seconds); } - } // @TODO else invalid? - return true; -} + g_string_append_printf(buf, " second%s", pcmk__plural_s(dt->seconds)); + } -/*! - * \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 \c true if valid time was parsed, \c 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; + // More than one minute, so provide a more readable breakdown into units + if (QB_ABS(dt->seconds) >= SECONDS_IN_MINUTE) { + uint32_t h = 0; + uint32_t m = 0; + uint32_t s = 0; + uint32_t u = QB_ABS(usec); + bool print_sec_component = false; - tzset(); + seconds_to_hms(dt->seconds, &h, &m, &s); + print_sec_component = ((s != 0) || (show_usec && (u != 0))); - if (time_str != NULL) { - if (!crm_time_parse_sec(time_str, &(a_time->seconds))) { - return false; - } + g_string_append(buf, " ("); - offset_s = strstr(time_str, "Z"); + if (h) { + g_string_append_printf(buf, "%" PRIu32 " hour%s", + h, pcmk__plural_s(h)); - /* @COMPAT: Spaces between the time and the offset are not supported - * by the standard according to section 3.4.1 and 4.2.5.2. - */ - if (offset_s == NULL) { - offset_s = strpbrk(time_str, " +-"); + if ((m != 0) || print_sec_component) { + g_string_append_c(buf, ' '); + } } - if (offset_s != NULL) { - while (isspace(*offset_s)) { - offset_s++; + if (m) { + g_string_append_printf(buf, "%" PRIu32 " minute%s", + m, pcmk__plural_s(m)); + + if (print_sec_component) { + g_string_append_c(buf, ' '); } } - } - if (!crm_time_parse_offset(offset_s, &(a_time->offset))) { - 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 (print_sec_component) { + if (show_usec) { + sec_usec_as_string(s, u, buf); + } else { + g_string_append_printf(buf, "%" PRIu32, s); + } + g_string_append_printf(buf, " second%s", + pcmk__plural_s(dt->seconds)); + } - 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); + g_string_append_c(buf, ')'); } - return true; } -/* +/*! * \internal - * \brief Parse a time object from an ISO 8601 date/time specification + * \brief Get a string representation of a time object * - * \param[in] date_str ISO 8601 date/time specification (or - * \c PCMK__VALUE_EPOCH) + * \param[in] dt Time to convert to string + * \param[in] usec Microseconds to add to \p dt + * \param[in] flags Group of \c crm_time_* string format options * - * \return New time object on success, NULL (and set errno) otherwise + * \return Newly allocated string representation of \p dt plus \p usec + * + * \note The caller is responsible for freeing the return value using \c free(). */ -static crm_time_t * -parse_date(const char *date_str) +static char * +time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags) { - const char *time_s = NULL; - crm_time_t *dt = NULL; + crm_time_t *utc = NULL; + GString *buf = NULL; + char *result = NULL; - uint32_t year = 0U; - uint32_t month = 0U; - uint32_t day = 0U; - uint32_t week = 0U; + if (!crm_time_is_defined(dt)) { + return pcmk__str_copy(""); + } - int rc = 0; + pcmk__assert(valid_sec_usec(dt->seconds, usec)); - if (pcmk__str_empty(date_str)) { - crm_err("No ISO 8601 date/time specification given"); - goto invalid; - } + buf = g_string_sized_new(128); - 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; + /* Simple cases: as duration, seconds, or seconds since epoch. + * These never depend on time zone. + */ + + if (pcmk__is_set(flags, crm_time_log_duration)) { + duration_as_string(dt, usec, pcmk__is_set(flags, crm_time_usecs), buf); + goto done; } - dt = crm_time_new_undefined(); + if (pcmk__any_flags_set(flags, crm_time_seconds|crm_time_epoch)) { + long long seconds = 0; - 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; - } + if (pcmk__is_set(flags, crm_time_seconds)) { + seconds = crm_time_get_seconds(dt); + } else { + seconds = crm_time_get_seconds_since_epoch(dt); + } - /* YYYY-MM-DD */ - rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32 "-%" SCNu32 "", - &year, &month, &day); - if (rc == 1) { - /* YYYYMMDD */ - rc = sscanf(date_str, "%4" SCNu32 "%2" SCNu32 "%2" SCNu32 "", - &year, &month, &day); - } - if (rc == 3) { - if ((month < 1U) || (month > 12U)) { - crm_err("'%s' is not a valid ISO 8601 date/time specification " - "because '%" PRIu32 "' is not a valid month", - date_str, month); - goto invalid; - } else 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 < 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; + if (pcmk__is_set(flags, crm_time_usecs)) { + sec_usec_as_string(seconds, usec, buf); } else { - dt->years = year; - dt->days = get_ordinal_days(year, month, day); - crm_trace("Parsed Gregorian date '%.4" PRIu32 "-%.3d' " - "from date string '%s'", year, dt->days, date_str); + g_string_append_printf(buf, "%lld", seconds); } - goto parse_time; + goto done; } - /* YYYY-DDD */ - rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32, &year, &day); - if (rc == 2) { - 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 '%" PRIu32 "' is not a valid day of year %" - PRIu32 " (1-%d)", - date_str, day, year, year_days(year)); - goto invalid; + // Convert to UTC if local timezone was not requested + if ((dt->offset != 0) && !pcmk__is_set(flags, crm_time_log_with_timezone)) { + utc = utc_from_crm_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 + if (dt->days > 0) { + uint32_t y = 0; + uint32_t w = 0; + uint32_t d = 0; + + pcmk__time_get_ywd(dt, &y, &w, &d); + g_string_append_printf(buf, + "%" 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)) { + g_string_append_printf(buf, "%" 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)) { + g_string_append_printf(buf, + "%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, + y, m, d); + } } - 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, "%" SCNu32 "-W%" SCNu32 "-%" SCNu32, - &year, &week, &day); - if (rc == 3) { - if ((week < 1U) || (week > crm_time_weeks_in_year(year))) { - crm_err("'%s' is not a valid ISO 8601 date/time specification " - "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 < 1U) || (day > 7U)) { - crm_err("'%s' is not a valid ISO 8601 date/time specification " - "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 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); + if (pcmk__is_set(flags, crm_time_log_timeofday)) { + uint32_t h = 0, m = 0, s = 0; - crm_trace("Parsed year %" PRIu32 " (Jan 1 = %d), week %" PRIu32 - ", and day %" PRIu32 " from date string '%s'", - year, jan1, week, day, date_str); + if (buf->len > 0) { + g_string_append_c(buf, ' '); + } - dt->years = year; - crm_time_add_days(dt, (week - 1) * 7); + if (crm_time_get_timeofday(dt, &h, &m, &s)) { + g_string_append_printf(buf, + "%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, + h, m, s); - if (jan1 <= 4) { - crm_time_add_days(dt, 1 - jan1); - } else { - crm_time_add_days(dt, 8 - jan1); + if (pcmk__is_set(flags, crm_time_usecs)) { + g_string_append_printf(buf, ".%06" PRIu32, QB_ABS(usec)); } - - 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; + if (pcmk__is_set(flags, crm_time_log_with_timezone) + && (dt->offset != 0)) { - parse_time: + seconds_to_hms(dt->offset, &h, &m, &s); + g_string_append_printf(buf, " %c%.2" PRIu32 ":%.2" PRIu32, + ((dt->offset < 0)? '-' : '+'), h, m); - 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; + g_string_append_c(buf, 'Z'); } } - if ((time_s != NULL) && !crm_time_parse(time_s, dt)) { - goto invalid; - } - crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday); - if (!crm_time_check(dt)) { - crm_err("'%s' is not a valid ISO 8601 date/time specification", - date_str); - goto invalid; - } - return dt; +done: + crm_time_free(utc); + result = pcmk__str_copy(buf->str); + g_string_free(buf, TRUE); + return result; +} -invalid: - crm_time_free(dt); - errno = EINVAL; - return NULL; +/*! + * \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) +{ + return time_as_string_common(dt, 0, flags); } // Parse an ISO 8601 numeric value and return number of characters consumed @@ -1213,7 +1212,7 @@ crm_time_parse_duration(const char *period_s) break; case 'H': - result = diff->seconds + (long long) an_int * HOUR_SECONDS; + result = diff->seconds + ((long long) an_int * SECONDS_IN_HOUR); if ((result < INT_MIN) || (result > INT_MAX)) { crm_err("'%s' is not a valid ISO 8601 time duration " "because integer at '%s' is too %s", @@ -1293,12 +1292,12 @@ crm_time_parse_period(const char *period_str) if (period_str[0] == 'P') { period->diff = crm_time_parse_duration(period_str); if (period->diff == NULL) { - goto error; + goto invalid; } } else { period->start = parse_date(period_str); if (period->start == NULL) { - goto error; + goto invalid; } } @@ -1314,18 +1313,18 @@ crm_time_parse_period(const char *period_str) } period->diff = crm_time_parse_duration(period_str); if (period->diff == NULL) { - goto error; + goto invalid; } } else { period->end = parse_date(period_str); if (period->end == NULL) { - goto error; + goto invalid; } } } else if (period->diff != NULL) { // Only duration given, assume start is now - period->start = crm_time_new(NULL); + period->start = pcmk__copy_timet(time(NULL)); } else { // Only start given @@ -1342,12 +1341,12 @@ crm_time_parse_period(const char *period_str) period->end = crm_time_add(period->start, period->diff); } - if (!crm_time_check(period->start)) { + if (!valid_time(period->start)) { 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)) { + if (!valid_time(period->end)) { crm_err("'%s' is not a valid ISO 8601 time period " "because the end is invalid", period_str); goto invalid; @@ -1356,7 +1355,6 @@ crm_time_parse_period(const char *period_str) invalid: errno = EINVAL; -error: crm_time_free_period(period); return NULL; } @@ -1377,76 +1375,6 @@ crm_time_free_period(crm_time_period_t *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; - crm_time_add_years(target, 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 @@ -1457,11 +1385,21 @@ crm_time_set_timet(crm_time_t *target, const time_t *source) 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); + const int flags = crm_time_log_date + |crm_time_log_timeofday + |crm_time_log_with_timezone; + + if ((target == NULL) + || (source == NULL) + || (crm_time_is_defined(target) + && (crm_time_compare(source, target) >= 0))) { + + return; } + + *target = *source; + pcmk__time_log(LOG_TRACE, "source", source, flags); + pcmk__time_log(LOG_TRACE, "target", target, flags); } crm_time_t * @@ -1469,24 +1407,63 @@ pcmk_copy_time(const crm_time_t *source) { crm_time_t *target = crm_time_new_undefined(); - crm_time_set(target, source); + *target = *source; return target; } /*! * \internal - * \brief Convert a \p time_t time to a \p crm_time_t time + * \brief Convert a \c time_t time to a \c crm_time_t time + * + * \param[in] source_sec Time to convert (as seconds since epoch) * - * \param[in] source Time to convert + * \return Newly allocated \c crm_time_t object representing \p source_sec * - * \return A \p crm_time_t object representing \p source + * \note The caller is responsible for freeing the return value using + * \c crm_time_free(). */ crm_time_t * -pcmk__copy_timet(time_t source) +pcmk__copy_timet(time_t source_sec) { + const struct tm *source = localtime(&source_sec); crm_time_t *target = crm_time_new_undefined(); - crm_time_set_timet(target, &source); + int h_offset = 0; + int m_offset = 0; + + if (source->tm_year > 0) { + // Years since 1900 + target->years = 1900; + crm_time_add_years(target, 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 += SECONDS_IN_HOUR * source->tm_hour; + } + + if (source->tm_min >= 0) { + target->seconds += SECONDS_IN_MINUTE * source->tm_min; + } + + if (source->tm_sec >= 0) { + target->seconds += source->tm_sec; + } + + // GMTOFF(source) == offset from UTC in seconds + h_offset = GMTOFF(source) / SECONDS_IN_HOUR; + m_offset = (GMTOFF(source) - (SECONDS_IN_HOUR * h_offset)) + / SECONDS_IN_MINUTE; + crm_trace("Time offset is %lds (%.2d:%.2d)", GMTOFF(source), h_offset, + m_offset); + + target->offset += SECONDS_IN_HOUR * h_offset; + target->offset += SECONDS_IN_MINUTE * m_offset; + return target; } @@ -1502,12 +1479,7 @@ crm_time_add(const crm_time_t *dt, const crm_time_t *value) } answer = pcmk_copy_time(dt); - - utc = crm_get_utc_time(value); - if (utc == NULL) { - crm_time_free(answer); - return NULL; - } + utc = utc_from_crm_time(value); crm_time_add_years(answer, utc->years); crm_time_add_months(answer, utc->months); @@ -1649,16 +1621,8 @@ crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value) 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; - } + utc = utc_from_crm_time(value); + answer = utc_from_crm_time(dt); answer->duration = TRUE; crm_time_add_years(answer, -utc->years); @@ -1683,12 +1647,9 @@ crm_time_subtract(const crm_time_t *dt, const crm_time_t *value) return NULL; } - utc = crm_get_utc_time(value); - if (utc == NULL) { - return NULL; - } - + utc = utc_from_crm_time(value); answer = pcmk_copy_time(dt); + crm_time_add_years(answer, -utc->years); if(utc->months != 0) { crm_time_add_months(answer, -utc->months); @@ -1700,21 +1661,6 @@ crm_time_subtract(const crm_time_t *dt, const crm_time_t *value) 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) { \ @@ -1732,20 +1678,25 @@ 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_t *t1 = NULL; + crm_time_t *t2 = NULL; + + if ((a == NULL) && (b == NULL)) { + return 0; } + if (a == NULL) { + return -1; + } + if (b == NULL) { + return 1; + } + + t1 = utc_from_crm_time(a); + t2 = utc_from_crm_time(b); + + 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); @@ -1761,23 +1712,23 @@ crm_time_compare(const crm_time_t *a, const crm_time_t *b) void crm_time_add_seconds(crm_time_t *a_time, int extra) { - int days = extra / DAY_SECONDS; + int days = extra / SECONDS_IN_DAY; pcmk__assert(a_time != NULL); crm_trace("Adding %d seconds (including %d whole day%s) to %d", extra, days, pcmk__plural_s(days), a_time->seconds); - a_time->seconds += extra % DAY_SECONDS; + a_time->seconds += extra % SECONDS_IN_DAY; // Check whether the addition crossed a day boundary - if (a_time->seconds > DAY_SECONDS) { + if (a_time->seconds > SECONDS_IN_DAY) { ++days; - a_time->seconds -= DAY_SECONDS; + a_time->seconds -= SECONDS_IN_DAY; } else if (a_time->seconds < 0) { --days; - a_time->seconds += DAY_SECONDS; + a_time->seconds += SECONDS_IN_DAY; } crm_time_add_days(a_time, days); @@ -1798,7 +1749,7 @@ crm_time_add_days(crm_time_t *a_time, int extra) if (extra > 0) { while ((a_time->days + (long long) extra) > year_days(a_time->years)) { - if ((a_time->years + 1LL) > INT_MAX) { + if (a_time->years == INT_MAX) { // Clip to latest we can handle a_time->days = year_days(a_time->years); return; @@ -1810,7 +1761,7 @@ crm_time_add_days(crm_time_t *a_time, int extra) const int min_days = a_time->duration? 0 : 1; while ((a_time->days + (long long) extra) < min_days) { - if ((a_time->years - 1) < 1) { + if (a_time->years <= 1) { a_time->days = 1; // Clip to earliest we can handle (no BCE) return; } @@ -1849,7 +1800,7 @@ crm_time_add_months(crm_time_t * a_time, int extra) } } - dmax = crm_time_days_in_month(m, y); + dmax = days_in_month_year(m, y); if (dmax < d) { /* Preserve day-of-month unless the month doesn't have enough days */ d = dmax; @@ -1867,13 +1818,13 @@ crm_time_add_months(crm_time_t * a_time, int extra) void crm_time_add_minutes(crm_time_t * a_time, int extra) { - crm_time_add_seconds(a_time, extra * 60); + crm_time_add_seconds(a_time, extra * SECONDS_IN_MINUTE); } void crm_time_add_hours(crm_time_t * a_time, int extra) { - crm_time_add_seconds(a_time, extra * HOUR_SECONDS); + crm_time_add_seconds(a_time, extra * SECONDS_IN_HOUR); } void @@ -1943,13 +1894,13 @@ get_g_date_time(const struct tm *tm, int offset) GTimeZone *tz = NULL; GDateTime *dt = NULL; - if (QB_ABS(offset) <= DAY_SECONDS) { + if (QB_ABS(offset) <= SECONDS_IN_DAY) { uint32_t hours = 0; uint32_t minutes = 0; uint32_t seconds = 0; int rc = 0; - crm_time_get_sec(offset, &hours, &minutes, &seconds); + seconds_to_hms(offset, &hours, &minutes, &seconds); rc = snprintf(buf, sizeof(buf), "%c%02" PRIu32 ":%02" PRIu32, ((offset >= 0)? '+' : '-'), hours, minutes); @@ -1958,7 +1909,7 @@ get_g_date_time(const struct tm *tm, int offset) } else { // offset out of range; use NULL as offset_s - CRM_LOG_ASSERT(QB_ABS(offset) <= DAY_SECONDS); + CRM_LOG_ASSERT(QB_ABS(offset) <= SECONDS_IN_DAY); } /* @FIXME @COMPAT As of glib 2.68, g_time_zone_new() is deprecated in favor @@ -2171,15 +2122,18 @@ char * pcmk__epoch2str(const time_t *source, uint32_t flags) { time_t epoch_time = (source == NULL)? time(NULL) : *source; + crm_time_t *dt = NULL; + char *result = NULL; if (flags == 0) { return pcmk__str_copy(g_strchomp(ctime(&epoch_time))); - } else { - crm_time_t dt; - - crm_time_set_timet(&dt, &epoch_time); - return crm_time_as_string(&dt, flags); } + + dt = pcmk__copy_timet(epoch_time); + result = crm_time_as_string(dt, flags); + + crm_time_free(dt); + return result; } /*! @@ -2203,14 +2157,19 @@ char * pcmk__timespec2str(const struct timespec *ts, uint32_t flags) { struct timespec tmp_ts; - crm_time_t dt; + crm_time_t *dt = NULL; + char *result = NULL; if (ts == NULL) { qb_util_timespec_from_epoch_get(&tmp_ts); ts = &tmp_ts; } - crm_time_set_timet(&dt, &ts->tv_sec); - return time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags); + + dt = pcmk__copy_timet(ts->tv_sec); + result = time_as_string_common(dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags); + + crm_time_free(dt); + return result; } /*! @@ -2228,9 +2187,9 @@ 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 MS_IN_M (MS_IN_S * SECONDS_IN_MINUTE) +#define MS_IN_H (MS_IN_M * MINUTES_IN_HOUR) +#define MS_IN_D (MS_IN_H * HOURS_IN_DAY) #define MAXSTR sizeof("..d..h..m..s...ms") static char str[MAXSTR]; GString *buf = NULL; @@ -2273,3 +2232,101 @@ pcmk__readable_interval(guint interval_ms) g_string_free(buf, TRUE); return str; } + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +bool +crm_time_leapyear(int year) +{ + return is_leap_year(year); +} + +int +crm_time_days_in_month(int month, int year) +{ + return days_in_month_year(month, year); +} + +int +crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m) +{ + uint32_t s; + + seconds_to_hms(dt->seconds, h, m, &s); + return TRUE; +} + +int +crm_time_weeks_in_year(int year) +{ + return weeks_in_year(year); +} + +int +crm_time_january1_weekday(int year) +{ + return jan1_day_of_week(year); +} + +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; + + pcmk__time_log(LOG_TRACE, "source", source, + crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); + pcmk__time_log(LOG_TRACE, "target", target, + crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); +} + +bool +crm_time_check(const crm_time_t *dt) +{ + return valid_time(dt); +} + +void +crm_time_set_timet(crm_time_t *target, const time_t *source_sec) +{ + crm_time_t *source = NULL; + + if (source_sec == NULL) { + return; + } + + source = pcmk__copy_timet(*source_sec); + *target = *source; + crm_time_free(source); +} + +int +crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w, + uint32_t *d) +{ + CRM_CHECK(dt->days > 0, return FALSE); + pcmk__time_get_ywd(dt, y, w, d); + return TRUE; +} + +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) +{ + pcmk__time_log_as(file, function, line, pcmk__clip_log_level(log_level), + prefix, date_time, flags); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/rules.c b/lib/common/rules.c index e97ce52cda6..5eafc66ca9a 100644 --- a/lib/common/rules.c +++ b/lib/common/rules.c @@ -199,8 +199,8 @@ pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now) crm_time_get_ordinal(now, &(ranges[0].value), &(ranges[6].value)); // Week year, week of week year, day of week - crm_time_get_isoweek(now, &(ranges[7].value), &(ranges[8].value), - &(ranges[9].value)); + pcmk__time_get_ywd(now, &(ranges[7].value), &(ranges[8].value), + &(ranges[9].value)); for (int i = 0; i < PCMK__NELEM(ranges); ++i) { int rc = check_range(date_spec, parent_id, ranges[i].attr, diff --git a/lib/common/tls.c b/lib/common/tls.c index a4616dd880e..b94fd741134 100644 --- a/lib/common/tls.c +++ b/lib/common/tls.c @@ -447,8 +447,8 @@ pcmk__tls_check_cert_expiration(gnutls_session_t session) if (expiry - now <= 60 * 60 * 24 * 30) { crm_time_t *expiry_t = pcmk__copy_timet(expiry); - crm_time_log(LOG_WARNING, "TLS certificate will expire on", - expiry_t, crm_time_log_date | crm_time_log_timeofday); + pcmk__time_log(LOG_WARNING, "TLS certificate will expire on", + expiry_t, crm_time_log_date|crm_time_log_timeofday); crm_time_free(expiry_t); } } diff --git a/lib/lrmd/lrmd_alerts.c b/lib/lrmd/lrmd_alerts.c index 392e099189f..f44a34351a8 100644 --- a/lib/lrmd/lrmd_alerts.c +++ b/lib/lrmd/lrmd_alerts.c @@ -128,11 +128,11 @@ exec_alert_list(lrmd_t *lrmd, const GList *alert_list, const char *kind_s = pcmk__alert_flag2text(kind); struct timespec now_tv = { 0, }; - crm_time_t now_dt = { 0, }; + crm_time_t *now_dt = NULL; int now_usec = 0; qb_util_timespec_from_epoch_get(&now_tv); - crm_time_set_timet(&now_dt, &(now_tv.tv_sec)); + now_dt = pcmk__copy_timet(now_tv.tv_sec); now_usec = now_tv.tv_nsec / QB_TIME_NS_IN_USEC; params = alert_key2param(params, PCMK__alert_key_kind, kind_s); @@ -173,7 +173,7 @@ exec_alert_list(lrmd_t *lrmd, const GList *alert_list, copy_params = alert_key2param(copy_params, PCMK__alert_key_recipient, entry->recipient); - str = pcmk__time_format_hr(entry->tstamp_format, &now_dt, now_usec); + str = pcmk__time_format_hr(entry->tstamp_format, now_dt, now_usec); if (str != NULL) { copy_params = alert_key2param(copy_params, PCMK__alert_key_timestamp, str); @@ -203,6 +203,8 @@ exec_alert_list(lrmd_t *lrmd, const GList *alert_list, } } + crm_time_free(now_dt); + if (any_failure) { return (any_success? -1 : -2); } diff --git a/lib/pacemaker/pcmk_sched_location.c b/lib/pacemaker/pcmk_sched_location.c index f66c9a5b8ee..9ad11824128 100644 --- a/lib/pacemaker/pcmk_sched_location.c +++ b/lib/pacemaker/pcmk_sched_location.c @@ -255,7 +255,7 @@ generate_location_rule(pcmk_resource_t *rsc, xmlNode *rule_xml, } location_rule = pcmk__new_location(rule_id, rsc, 0, discovery, NULL); - CRM_CHECK(location_rule != NULL, return NULL); + CRM_CHECK(location_rule != NULL, return false); location_rule->role_filter = role; diff --git a/lib/pacemaker/pcmk_simulate.c b/lib/pacemaker/pcmk_simulate.c index 642aa3c2299..4949165c03e 100644 --- a/lib/pacemaker/pcmk_simulate.c +++ b/lib/pacemaker/pcmk_simulate.c @@ -507,8 +507,8 @@ set_effective_date(pcmk_scheduler_t *scheduler, bool print_original, if (use_date) { scheduler->priv->now = crm_time_new(use_date); out->info(out, "Setting effective cluster time: %s", use_date); - crm_time_log(LOG_NOTICE, "Pretending 'now' is", scheduler->priv->now, - crm_time_log_date | crm_time_log_timeofday); + pcmk__time_log(LOG_NOTICE, "Pretending 'now' is", scheduler->priv->now, + crm_time_log_date|crm_time_log_timeofday); } else if (original_date != 0) { scheduler->priv->now = pcmk__copy_timet(original_date); diff --git a/tools/crm_resource_ban.c b/tools/crm_resource_ban.c index 6fefe4b0b7b..ac9c29a978c 100644 --- a/tools/crm_resource_ban.c +++ b/tools/crm_resource_ban.c @@ -22,6 +22,8 @@ parse_cli_lifetime(pcmk__output_t *out, const char *move_lifetime) crm_time_t *now = NULL; crm_time_t *later = NULL; crm_time_t *duration = NULL; + const uint32_t duration_flags = crm_time_log_date|crm_time_log_timeofday; + const uint32_t time_flags = duration_flags|crm_time_log_with_timezone; if (move_lifetime == NULL) { return NULL; @@ -46,12 +48,10 @@ parse_cli_lifetime(pcmk__output_t *out, const char *move_lifetime) return NULL; } - crm_time_log(LOG_INFO, "now ", now, - crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); - crm_time_log(LOG_INFO, "later ", later, - crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); - crm_time_log(LOG_INFO, "duration", duration, crm_time_log_date | crm_time_log_timeofday); - later_s = crm_time_as_string(later, crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); + pcmk__time_log(LOG_INFO, "now ", now, time_flags); + pcmk__time_log(LOG_INFO, "later ", later, time_flags); + pcmk__time_log(LOG_INFO, "duration", duration, duration_flags); + later_s = crm_time_as_string(later, time_flags); out->info(out, "Migration will take effect until: %s", later_s); crm_time_free(duration);