root/lib/common/iso8601.c

/* [previous][next][first][last][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. crm_get_utc_time
  2. crm_time_new
  3. crm_time_new_undefined
  4. crm_time_is_defined
  5. crm_time_free
  6. year_days
  7. crm_time_january1_weekday
  8. crm_time_weeks_in_year
  9. crm_time_days_in_month
  10. crm_time_leapyear
  11. get_ordinal_days
  12. crm_time_log_alias
  13. crm_time_get_sec
  14. crm_time_get_timeofday
  15. crm_time_get_timezone
  16. crm_time_get_seconds
  17. crm_time_get_seconds_since_epoch
  18. crm_time_get_gregorian
  19. crm_time_get_ordinal
  20. crm_time_get_isoweek
  21. sec_usec_as_string
  22. crm_duration_as_string
  23. time_as_string_common
  24. crm_time_as_string
  25. crm_time_parse_sec
  26. crm_time_parse_offset
  27. crm_time_parse
  28. parse_date
  29. parse_int
  30. crm_time_parse_duration
  31. crm_time_parse_period
  32. crm_time_free_period
  33. crm_time_set
  34. ha_set_tm_time
  35. crm_time_set_timet
  36. pcmk__set_time_if_earlier
  37. pcmk_copy_time
  38. pcmk__copy_timet
  39. crm_time_add
  40. pcmk__time_component_attr
  41. component_fn
  42. pcmk__add_time_from_xml
  43. crm_time_calculate_duration
  44. crm_time_subtract
  45. crm_time_check
  46. crm_time_compare
  47. crm_time_add_seconds
  48. crm_time_add_days
  49. crm_time_add_months
  50. crm_time_add_minutes
  51. crm_time_add_hours
  52. crm_time_add_weeks
  53. crm_time_add_years
  54. ha_get_tm_time
  55. pcmk__time_hr_convert
  56. pcmk__time_set_hr_dt
  57. pcmk__time_hr_now
  58. pcmk__time_hr_new
  59. pcmk__time_hr_free
  60. pcmk__time_format_hr
  61. pcmk__epoch2str
  62. pcmk__timespec2str
  63. pcmk__readable_interval

   1 /*
   2  * Copyright 2005-2024 the Pacemaker project contributors
   3  *
   4  * The version control history for this file may have further details.
   5  *
   6  * This source code is licensed under the GNU Lesser General Public License
   7  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
   8  */
   9 
  10 /*
  11  * References:
  12  *      https://en.wikipedia.org/wiki/ISO_8601
  13  *      http://www.staff.science.uu.nl/~gent0113/calendar/isocalendar.htm
  14  */
  15 
  16 #include <crm_internal.h>
  17 #include <crm/crm.h>
  18 #include <time.h>
  19 #include <ctype.h>
  20 #include <inttypes.h>
  21 #include <limits.h>         // INT_MIN, INT_MAX
  22 #include <string.h>
  23 #include <stdbool.h>
  24 #include <crm/common/iso8601.h>
  25 #include <crm/common/iso8601_internal.h>
  26 #include "crmcommon_private.h"
  27 
  28 /*
  29  * Andrew's code was originally written for OSes whose "struct tm" contains:
  30  *      long tm_gmtoff;         :: Seconds east of UTC
  31  *      const char *tm_zone;    :: Timezone abbreviation
  32  * Some OSes lack these, instead having:
  33  *      time_t (or long) timezone;
  34                 :: "difference between UTC and local standard time"
  35  *      char *tzname[2] = { "...", "..." };
  36  * I (David Lee) confess to not understanding the details.  So my attempted
  37  * generalisations for where their use is necessary may be flawed.
  38  *
  39  * 1. Does "difference between ..." subtract the same or opposite way?
  40  * 2. Should it use "altzone" instead of "timezone"?
  41  * 3. Should it use tzname[0] or tzname[1]?  Interaction with timezone/altzone?
  42  */
  43 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
  44 #  define GMTOFF(tm) ((tm)->tm_gmtoff)
  45 #else
  46 /* Note: extern variable; macro argument not actually used.  */
  47 #  define GMTOFF(tm) (-timezone+daylight)
  48 #endif
  49 
  50 #define HOUR_SECONDS    (60 * 60)
  51 #define DAY_SECONDS     (HOUR_SECONDS * 24)
  52 
  53 /*!
  54  * \internal
  55  * \brief Validate a seconds/microseconds tuple
  56  *
  57  * The microseconds value must be in the correct range, and if both are nonzero
  58  * they must have the same sign.
  59  *
  60  * \param[in] sec   Seconds
  61  * \param[in] usec  Microseconds
  62  *
  63  * \return true if the seconds/microseconds tuple is valid, or false otherwise
  64  */
  65 #define valid_sec_usec(sec, usec)               \
  66         ((QB_ABS(usec) < QB_TIME_US_IN_SEC)     \
  67          && (((sec) == 0) || ((usec) == 0) || (((sec) < 0) == ((usec) < 0))))
  68 
  69 // A date/time or duration
  70 struct crm_time_s {
  71     int years;      // Calendar year (date/time) or number of years (duration)
  72     int months;     // Number of months (duration only)
  73     int days;       // Ordinal day of year (date/time) or number of days (duration)
  74     int seconds;    // Seconds of day (date/time) or number of seconds (duration)
  75     int offset;     // Seconds offset from UTC (date/time only)
  76     bool duration;  // True if duration
  77 };
  78 
  79 static crm_time_t *parse_date(const char *date_str);
  80 
  81 static crm_time_t *
  82 crm_get_utc_time(const crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
  83 {
  84     crm_time_t *utc = NULL;
  85 
  86     if (dt == NULL) {
  87         errno = EINVAL;
  88         return NULL;
  89     }
  90 
  91     utc = crm_time_new_undefined();
  92     utc->years = dt->years;
  93     utc->days = dt->days;
  94     utc->seconds = dt->seconds;
  95     utc->offset = 0;
  96 
  97     if (dt->offset) {
  98         crm_time_add_seconds(utc, -dt->offset);
  99     } else {
 100         /* Durations (which are the only things that can include months, never have a timezone */
 101         utc->months = dt->months;
 102     }
 103 
 104     crm_time_log(LOG_TRACE, "utc-source", dt,
 105                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
 106     crm_time_log(LOG_TRACE, "utc-target", utc,
 107                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
 108     return utc;
 109 }
 110 
 111 crm_time_t *
 112 crm_time_new(const char *date_time)
     /* [previous][next][first][last][top][bottom][index][help] */
 113 {
 114     tzset();
 115     if (date_time == NULL) {
 116         return pcmk__copy_timet(time(NULL));
 117     }
 118     return parse_date(date_time);
 119 }
 120 
 121 /*!
 122  * \brief Allocate memory for an uninitialized time object
 123  *
 124  * \return Newly allocated time object
 125  * \note The caller is responsible for freeing the return value using
 126  *       crm_time_free().
 127  */
 128 crm_time_t *
 129 crm_time_new_undefined(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 130 {
 131     return (crm_time_t *) pcmk__assert_alloc(1, sizeof(crm_time_t));
 132 }
 133 
 134 /*!
 135  * \brief Check whether a time object has been initialized yet
 136  *
 137  * \param[in] t  Time object to check
 138  *
 139  * \return TRUE if time object has been initialized, FALSE otherwise
 140  */
 141 bool
 142 crm_time_is_defined(const crm_time_t *t)
     /* [previous][next][first][last][top][bottom][index][help] */
 143 {
 144     // Any nonzero member indicates something has been done to t
 145     return (t != NULL) && (t->years || t->months || t->days || t->seconds
 146                            || t->offset || t->duration);
 147 }
 148 
 149 void
 150 crm_time_free(crm_time_t * dt)
     /* [previous][next][first][last][top][bottom][index][help] */
 151 {
 152     if (dt == NULL) {
 153         return;
 154     }
 155     free(dt);
 156 }
 157 
 158 static int
 159 year_days(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 160 {
 161     int d = 365;
 162 
 163     if (crm_time_leapyear(year)) {
 164         d++;
 165     }
 166     return d;
 167 }
 168 
 169 /* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt :
 170  *
 171  * 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7)
 172  *  YY = (Y-1) % 100
 173  *  C = (Y-1) - YY
 174  *  G = YY + YY/4
 175  *  Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7)
 176  */
 177 int
 178 crm_time_january1_weekday(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 179 {
 180     int YY = (year - 1) % 100;
 181     int C = (year - 1) - YY;
 182     int G = YY + YY / 4;
 183     int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7);
 184 
 185     crm_trace("YY=%d, C=%d, G=%d", YY, C, G);
 186     crm_trace("January 1 %.4d: %d", year, jan1);
 187     return jan1;
 188 }
 189 
 190 int
 191 crm_time_weeks_in_year(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 192 {
 193     int weeks = 52;
 194     int jan1 = crm_time_january1_weekday(year);
 195 
 196     /* if jan1 == thursday */
 197     if (jan1 == 4) {
 198         weeks++;
 199     } else {
 200         jan1 = crm_time_january1_weekday(year + 1);
 201         /* if dec31 == thursday aka. jan1 of next year is a friday */
 202         if (jan1 == 5) {
 203             weeks++;
 204         }
 205 
 206     }
 207     return weeks;
 208 }
 209 
 210 // Jan-Dec plus Feb of leap years
 211 static int month_days[13] = {
 212     31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29
 213 };
 214 
 215 /*!
 216  * \brief Return number of days in given month of given year
 217  *
 218  * \param[in]  Ordinal month (1-12)
 219  * \param[in]  Gregorian year
 220  *
 221  * \return Number of days in given month (0 if given month is invalid)
 222  */
 223 int
 224 crm_time_days_in_month(int month, int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 225 {
 226     if ((month < 1) || (month > 12)) {
 227         return 0;
 228     }
 229     if ((month == 2) && crm_time_leapyear(year)) {
 230         month = 13;
 231     }
 232     return month_days[month - 1];
 233 }
 234 
 235 bool
 236 crm_time_leapyear(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 237 {
 238     gboolean is_leap = FALSE;
 239 
 240     if (year % 4 == 0) {
 241         is_leap = TRUE;
 242     }
 243     if (year % 100 == 0 && year % 400 != 0) {
 244         is_leap = FALSE;
 245     }
 246     return is_leap;
 247 }
 248 
 249 static uint32_t
 250 get_ordinal_days(uint32_t y, uint32_t m, uint32_t d)
     /* [previous][next][first][last][top][bottom][index][help] */
 251 {
 252     int lpc;
 253 
 254     for (lpc = 1; lpc < m; lpc++) {
 255         d += crm_time_days_in_month(lpc, y);
 256     }
 257     return d;
 258 }
 259 
 260 void
 261 crm_time_log_alias(int log_level, const char *file, const char *function,
     /* [previous][next][first][last][top][bottom][index][help] */
 262                    int line, const char *prefix, const crm_time_t *date_time,
 263                    int flags)
 264 {
 265     char *date_s = crm_time_as_string(date_time, flags);
 266 
 267     if (log_level == LOG_STDOUT) {
 268         printf("%s%s%s\n",
 269                (prefix? prefix : ""), (prefix? ": " : ""), date_s);
 270     } else {
 271         do_crm_log_alias(log_level, file, function, line, "%s%s%s",
 272                          (prefix? prefix : ""), (prefix? ": " : ""), date_s);
 273     }
 274     free(date_s);
 275 }
 276 
 277 static void
 278 crm_time_get_sec(int sec, uint32_t *h, uint32_t *m, uint32_t *s)
     /* [previous][next][first][last][top][bottom][index][help] */
 279 {
 280     uint32_t hours, minutes, seconds;
 281 
 282     seconds = QB_ABS(sec);
 283 
 284     hours = seconds / HOUR_SECONDS;
 285     seconds -= HOUR_SECONDS * hours;
 286 
 287     minutes = seconds / 60;
 288     seconds -= 60 * minutes;
 289 
 290     crm_trace("%d == %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
 291               sec, hours, minutes, seconds);
 292 
 293     *h = hours;
 294     *m = minutes;
 295     *s = seconds;
 296 }
 297 
 298 int
 299 crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m,
     /* [previous][next][first][last][top][bottom][index][help] */
 300                        uint32_t *s)
 301 {
 302     crm_time_get_sec(dt->seconds, h, m, s);
 303     return TRUE;
 304 }
 305 
 306 int
 307 crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m)
     /* [previous][next][first][last][top][bottom][index][help] */
 308 {
 309     uint32_t s;
 310 
 311     crm_time_get_sec(dt->seconds, h, m, &s);
 312     return TRUE;
 313 }
 314 
 315 long long
 316 crm_time_get_seconds(const crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
 317 {
 318     int lpc;
 319     crm_time_t *utc = NULL;
 320     long long in_seconds = 0;
 321 
 322     if (dt == NULL) {
 323         return 0;
 324     }
 325 
 326     utc = crm_get_utc_time(dt);
 327     if (utc == NULL) {
 328         return 0;
 329     }
 330 
 331     for (lpc = 1; lpc < utc->years; lpc++) {
 332         long long dmax = year_days(lpc);
 333 
 334         in_seconds += DAY_SECONDS * dmax;
 335     }
 336 
 337     /* utc->months is an offset that can only be set for a duration.
 338      * By definition, the value is variable depending on the date to
 339      * which it is applied.
 340      *
 341      * Force 30-day months so that something vaguely sane happens
 342      * for anyone that tries to use a month in this way.
 343      */
 344     if (utc->months > 0) {
 345         in_seconds += DAY_SECONDS * 30 * (long long) (utc->months);
 346     }
 347 
 348     if (utc->days > 0) {
 349         in_seconds += DAY_SECONDS * (long long) (utc->days - 1);
 350     }
 351     in_seconds += utc->seconds;
 352 
 353     crm_time_free(utc);
 354     return in_seconds;
 355 }
 356 
 357 #define EPOCH_SECONDS 62135596800ULL    /* Calculated using crm_time_get_seconds() */
 358 long long
 359 crm_time_get_seconds_since_epoch(const crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
 360 {
 361     return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS);
 362 }
 363 
 364 int
 365 crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m,
     /* [previous][next][first][last][top][bottom][index][help] */
 366                        uint32_t *d)
 367 {
 368     int months = 0;
 369     int days = dt->days;
 370 
 371     if(dt->years != 0) {
 372         for (months = 1; months <= 12 && days > 0; months++) {
 373             int mdays = crm_time_days_in_month(months, dt->years);
 374 
 375             if (mdays >= days) {
 376                 break;
 377             } else {
 378                 days -= mdays;
 379             }
 380         }
 381 
 382     } else if (dt->months) {
 383         /* This is a duration including months, don't convert the days field */
 384         months = dt->months;
 385 
 386     } else {
 387         /* This is a duration not including months, still don't convert the days field */
 388     }
 389 
 390     *y = dt->years;
 391     *m = months;
 392     *d = days;
 393     crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days);
 394     return TRUE;
 395 }
 396 
 397 int
 398 crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d)
     /* [previous][next][first][last][top][bottom][index][help] */
 399 {
 400     *y = dt->years;
 401     *d = dt->days;
 402     return TRUE;
 403 }
 404 
 405 int
 406 crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w,
     /* [previous][next][first][last][top][bottom][index][help] */
 407                      uint32_t *d)
 408 {
 409     /*
 410      * Monday 29 December 2008 is written "2009-W01-1"
 411      * Sunday 3 January 2010 is written "2009-W53-7"
 412      */
 413     int year_num = 0;
 414     int jan1 = crm_time_january1_weekday(dt->years);
 415     int h = -1;
 416 
 417     CRM_CHECK(dt->days > 0, return FALSE);
 418 
 419 /* 6. Find the Weekday for Y M D */
 420     h = dt->days + jan1 - 1;
 421     *d = 1 + ((h - 1) % 7);
 422 
 423 /* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */
 424     if (dt->days <= (8 - jan1) && jan1 > 4) {
 425         crm_trace("year--, jan1=%d", jan1);
 426         year_num = dt->years - 1;
 427         *w = crm_time_weeks_in_year(year_num);
 428 
 429     } else {
 430         year_num = dt->years;
 431     }
 432 
 433 /* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */
 434     if (year_num == dt->years) {
 435         int dmax = year_days(year_num);
 436         int correction = 4 - *d;
 437 
 438         if ((dmax - dt->days) < correction) {
 439             crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction);
 440             year_num = dt->years + 1;
 441             *w = 1;
 442         }
 443     }
 444 
 445 /* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */
 446     if (year_num == dt->years) {
 447         int j = dt->days + (7 - *d) + (jan1 - 1);
 448 
 449         *w = j / 7;
 450         if (jan1 > 4) {
 451             *w -= 1;
 452         }
 453     }
 454 
 455     *y = year_num;
 456     crm_trace("Converted %.4d-%.3d to %.4" PRIu32 "-W%.2" PRIu32 "-%" PRIu32,
 457               dt->years, dt->days, *y, *w, *d);
 458     return TRUE;
 459 }
 460 
 461 #define DATE_MAX 128
 462 
 463 /*!
 464  * \internal
 465  * \brief Print "<seconds>.<microseconds>" to a buffer
 466  *
 467  * \param[in]     sec     Seconds
 468  * \param[in]     usec    Microseconds (must be of same sign as \p sec and of
 469  *                        absolute value less than \p QB_TIME_US_IN_SEC)
 470  * \param[in,out] buf     Result buffer
 471  * \param[in,out] offset  Current offset within \p buf
 472  */
 473 static inline void
 474 sec_usec_as_string(long long sec, int usec, char *buf, size_t *offset)
     /* [previous][next][first][last][top][bottom][index][help] */
 475 {
 476     *offset += snprintf(buf + *offset, DATE_MAX - *offset, "%s%lld.%06d",
 477                         ((sec == 0) && (usec < 0))? "-" : "",
 478                         sec, QB_ABS(usec));
 479 }
 480 
 481 /*!
 482  * \internal
 483  * \brief Get a string representation of a duration
 484  *
 485  * \param[in]  dt         Time object to interpret as a duration
 486  * \param[in]  usec       Microseconds to add to \p dt
 487  * \param[in]  show_usec  Whether to include microseconds in \p result
 488  * \param[out] result     Where to store the result string
 489  */
 490 static void
 491 crm_duration_as_string(const crm_time_t *dt, int usec, bool show_usec,
     /* [previous][next][first][last][top][bottom][index][help] */
 492                        char *result)
 493 {
 494     size_t offset = 0;
 495 
 496     CRM_ASSERT(valid_sec_usec(dt->seconds, usec));
 497 
 498     if (dt->years) {
 499         offset += snprintf(result + offset, DATE_MAX - offset, "%4d year%s ",
 500                            dt->years, pcmk__plural_s(dt->years));
 501     }
 502     if (dt->months) {
 503         offset += snprintf(result + offset, DATE_MAX - offset, "%2d month%s ",
 504                            dt->months, pcmk__plural_s(dt->months));
 505     }
 506     if (dt->days) {
 507         offset += snprintf(result + offset, DATE_MAX - offset, "%2d day%s ",
 508                            dt->days, pcmk__plural_s(dt->days));
 509     }
 510 
 511     // At least print seconds (and optionally usecs)
 512     if ((offset == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) {
 513         if (show_usec) {
 514             sec_usec_as_string(dt->seconds, usec, result, &offset);
 515         } else {
 516             offset += snprintf(result + offset, DATE_MAX - offset, "%d",
 517                                dt->seconds);
 518         }
 519         offset += snprintf(result + offset, DATE_MAX - offset, " second%s",
 520                            pcmk__plural_s(dt->seconds));
 521     }
 522 
 523     // More than one minute, so provide a more readable breakdown into units
 524     if (QB_ABS(dt->seconds) >= 60) {
 525         uint32_t h = 0;
 526         uint32_t m = 0;
 527         uint32_t s = 0;
 528         uint32_t u = QB_ABS(usec);
 529         bool print_sec_component = false;
 530 
 531         crm_time_get_sec(dt->seconds, &h, &m, &s);
 532         print_sec_component = ((s != 0) || (show_usec && (u != 0)));
 533 
 534         offset += snprintf(result + offset, DATE_MAX - offset, " (");
 535 
 536         if (h) {
 537             offset += snprintf(result + offset, DATE_MAX - offset,
 538                                "%" PRIu32 " hour%s%s", h, pcmk__plural_s(h),
 539                                ((m != 0) || print_sec_component)? " " : "");
 540         }
 541 
 542         if (m) {
 543             offset += snprintf(result + offset, DATE_MAX - offset,
 544                                "%" PRIu32 " minute%s%s", m, pcmk__plural_s(m),
 545                                print_sec_component? " " : "");
 546         }
 547 
 548         if (print_sec_component) {
 549             if (show_usec) {
 550                 sec_usec_as_string(s, u, result, &offset);
 551             } else {
 552                 offset += snprintf(result + offset, DATE_MAX - offset,
 553                                    "%" PRIu32, s);
 554             }
 555             offset += snprintf(result + offset, DATE_MAX - offset, " second%s",
 556                                pcmk__plural_s(dt->seconds));
 557         }
 558 
 559         offset += snprintf(result + offset, DATE_MAX - offset, ")");
 560     }
 561 }
 562 
 563 /*!
 564  * \internal
 565  * \brief Get a string representation of a time object
 566  *
 567  * \param[in]  dt      Time to convert to string
 568  * \param[in]  usec    Microseconds to add to \p dt
 569  * \param[in]  flags   Group of \p crm_time_* string format options
 570  * \param[out] result  Where to store the result string
 571  *
 572  * \note \p result must be of size \p DATE_MAX or larger.
 573  */
 574 static void
 575 time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags,
     /* [previous][next][first][last][top][bottom][index][help] */
 576                       char *result)
 577 {
 578     crm_time_t *utc = NULL;
 579     size_t offset = 0;
 580 
 581     if (!crm_time_is_defined(dt)) {
 582         strcpy(result, "<undefined time>");
 583         return;
 584     }
 585 
 586     CRM_ASSERT(valid_sec_usec(dt->seconds, usec));
 587 
 588     /* Simple cases: as duration, seconds, or seconds since epoch.
 589      * These never depend on time zone.
 590      */
 591 
 592     if (pcmk_is_set(flags, crm_time_log_duration)) {
 593         crm_duration_as_string(dt, usec, pcmk_is_set(flags, crm_time_usecs),
 594                                result);
 595         return;
 596     }
 597 
 598     if (pcmk_any_flags_set(flags, crm_time_seconds|crm_time_epoch)) {
 599         long long seconds = 0;
 600 
 601         if (pcmk_is_set(flags, crm_time_seconds)) {
 602             seconds = crm_time_get_seconds(dt);
 603         } else {
 604             seconds = crm_time_get_seconds_since_epoch(dt);
 605         }
 606 
 607         if (pcmk_is_set(flags, crm_time_usecs)) {
 608             sec_usec_as_string(seconds, usec, result, &offset);
 609         } else {
 610             snprintf(result, DATE_MAX, "%lld", seconds);
 611         }
 612         return;
 613     }
 614 
 615     // Convert to UTC if local timezone was not requested
 616     if ((dt->offset != 0) && !pcmk_is_set(flags, crm_time_log_with_timezone)) {
 617         crm_trace("UTC conversion");
 618         utc = crm_get_utc_time(dt);
 619         dt = utc;
 620     }
 621 
 622     // As readable string
 623 
 624     if (pcmk_is_set(flags, crm_time_log_date)) {
 625         if (pcmk_is_set(flags, crm_time_weeks)) { // YYYY-WW-D
 626             uint32_t y = 0;
 627             uint32_t w = 0;
 628             uint32_t d = 0;
 629 
 630             if (crm_time_get_isoweek(dt, &y, &w, &d)) {
 631                 offset += snprintf(result + offset, DATE_MAX - offset,
 632                                    "%" PRIu32 "-W%.2" PRIu32 "-%" PRIu32,
 633                                    y, w, d);
 634             }
 635 
 636         } else if (pcmk_is_set(flags, crm_time_ordinal)) { // YYYY-DDD
 637             uint32_t y = 0;
 638             uint32_t d = 0;
 639 
 640             if (crm_time_get_ordinal(dt, &y, &d)) {
 641                 offset += snprintf(result + offset, DATE_MAX - offset,
 642                                    "%" PRIu32 "-%.3" PRIu32, y, d);
 643             }
 644 
 645         } else { // YYYY-MM-DD
 646             uint32_t y = 0;
 647             uint32_t m = 0;
 648             uint32_t d = 0;
 649 
 650             if (crm_time_get_gregorian(dt, &y, &m, &d)) {
 651                 offset += snprintf(result + offset, DATE_MAX - offset,
 652                                    "%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32,
 653                                    y, m, d);
 654             }
 655         }
 656     }
 657 
 658     if (pcmk_is_set(flags, crm_time_log_timeofday)) {
 659         uint32_t h = 0, m = 0, s = 0;
 660 
 661         if (offset > 0) {
 662             offset += snprintf(result + offset, DATE_MAX - offset, " ");
 663         }
 664 
 665         if (crm_time_get_timeofday(dt, &h, &m, &s)) {
 666             offset += snprintf(result + offset, DATE_MAX - offset,
 667                                "%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
 668                                h, m, s);
 669 
 670             if (pcmk_is_set(flags, crm_time_usecs)) {
 671                 offset += snprintf(result + offset, DATE_MAX - offset,
 672                                    ".%06" PRIu32, QB_ABS(usec));
 673             }
 674         }
 675 
 676         if (pcmk_is_set(flags, crm_time_log_with_timezone)
 677             && (dt->offset != 0)) {
 678             crm_time_get_sec(dt->offset, &h, &m, &s);
 679             offset += snprintf(result + offset, DATE_MAX - offset,
 680                                " %c%.2" PRIu32 ":%.2" PRIu32,
 681                                ((dt->offset < 0)? '-' : '+'), h, m);
 682         } else {
 683             offset += snprintf(result + offset, DATE_MAX - offset, "Z");
 684         }
 685     }
 686 
 687     crm_time_free(utc);
 688 }
 689 
 690 /*!
 691  * \brief Get a string representation of a \p crm_time_t object
 692  *
 693  * \param[in]  dt      Time to convert to string
 694  * \param[in]  flags   Group of \p crm_time_* string format options
 695  *
 696  * \note The caller is responsible for freeing the return value using \p free().
 697  */
 698 char *
 699 crm_time_as_string(const crm_time_t *dt, int flags)
     /* [previous][next][first][last][top][bottom][index][help] */
 700 {
 701     char result[DATE_MAX] = { '\0', };
 702 
 703     time_as_string_common(dt, 0, flags, result);
 704     return pcmk__str_copy(result);
 705 }
 706 
 707 /*!
 708  * \internal
 709  * \brief Determine number of seconds from an hour:minute:second string
 710  *
 711  * \param[in]  time_str  Time specification string
 712  * \param[out] result    Number of seconds equivalent to time_str
 713  *
 714  * \return TRUE if specification was valid, FALSE (and set errno) otherwise
 715  * \note This may return the number of seconds in a day (which is out of bounds
 716  *       for a time object) if given 24:00:00.
 717  */
 718 static bool
 719 crm_time_parse_sec(const char *time_str, int *result)
     /* [previous][next][first][last][top][bottom][index][help] */
 720 {
 721     int rc;
 722     uint32_t hour = 0;
 723     uint32_t minute = 0;
 724     uint32_t second = 0;
 725 
 726     *result = 0;
 727 
 728     // Must have at least hour, but minutes and seconds are optional
 729     rc = sscanf(time_str, "%" SCNu32 ":%" SCNu32 ":%" SCNu32,
 730                 &hour, &minute, &second);
 731     if (rc == 1) {
 732         rc = sscanf(time_str, "%2" SCNu32 "%2" SCNu32 "%2" SCNu32,
 733                     &hour, &minute, &second);
 734     }
 735     if (rc == 0) {
 736         crm_err("%s is not a valid ISO 8601 time specification", time_str);
 737         errno = EINVAL;
 738         return FALSE;
 739     }
 740 
 741     crm_trace("Got valid time: %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
 742               hour, minute, second);
 743 
 744     if ((hour == 24) && (minute == 0) && (second == 0)) {
 745         // Equivalent to 00:00:00 of next day, return number of seconds in day
 746     } else if (hour >= 24) {
 747         crm_err("%s is not a valid ISO 8601 time specification "
 748                 "because %" PRIu32 " is not a valid hour", time_str, hour);
 749         errno = EINVAL;
 750         return FALSE;
 751     }
 752     if (minute >= 60) {
 753         crm_err("%s is not a valid ISO 8601 time specification "
 754                 "because %" PRIu32 " is not a valid minute", time_str, minute);
 755         errno = EINVAL;
 756         return FALSE;
 757     }
 758     if (second >= 60) {
 759         crm_err("%s is not a valid ISO 8601 time specification "
 760                 "because %" PRIu32 " is not a valid second", time_str, second);
 761         errno = EINVAL;
 762         return FALSE;
 763     }
 764 
 765     *result = (hour * HOUR_SECONDS) + (minute * 60) + second;
 766     return TRUE;
 767 }
 768 
 769 static bool
 770 crm_time_parse_offset(const char *offset_str, int *offset)
     /* [previous][next][first][last][top][bottom][index][help] */
 771 {
 772     tzset();
 773 
 774     if (offset_str == NULL) {
 775         // Use local offset
 776 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
 777         time_t now = time(NULL);
 778         struct tm *now_tm = localtime(&now);
 779 #endif
 780         int h_offset = GMTOFF(now_tm) / HOUR_SECONDS;
 781         int m_offset = (GMTOFF(now_tm) - (HOUR_SECONDS * h_offset)) / 60;
 782 
 783         if (h_offset < 0 && m_offset < 0) {
 784             m_offset = 0 - m_offset;
 785         }
 786         *offset = (HOUR_SECONDS * h_offset) + (60 * m_offset);
 787         return TRUE;
 788     }
 789 
 790     if (offset_str[0] == 'Z') { // @TODO invalid if anything after?
 791         *offset = 0;
 792         return TRUE;
 793     }
 794 
 795     *offset = 0;
 796     if ((offset_str[0] == '+') || (offset_str[0] == '-')
 797         || isdigit((int)offset_str[0])) {
 798 
 799         gboolean negate = FALSE;
 800 
 801         if (offset_str[0] == '+') {
 802             offset_str++;
 803         } else if (offset_str[0] == '-') {
 804             negate = TRUE;
 805             offset_str++;
 806         }
 807         if (crm_time_parse_sec(offset_str, offset) == FALSE) {
 808             return FALSE;
 809         }
 810         if (negate) {
 811             *offset = 0 - *offset;
 812         }
 813     } // @TODO else invalid?
 814     return TRUE;
 815 }
 816 
 817 /*!
 818  * \internal
 819  * \brief Parse the time portion of an ISO 8601 date/time string
 820  *
 821  * \param[in]     time_str  Time portion of specification (after any 'T')
 822  * \param[in,out] a_time    Time object to parse into
 823  *
 824  * \return TRUE if valid time was parsed, FALSE (and set errno) otherwise
 825  * \note This may add a day to a_time (if the time is 24:00:00).
 826  */
 827 static bool
 828 crm_time_parse(const char *time_str, crm_time_t *a_time)
     /* [previous][next][first][last][top][bottom][index][help] */
 829 {
 830     uint32_t h, m, s;
 831     char *offset_s = NULL;
 832 
 833     tzset();
 834 
 835     if (time_str) {
 836         if (crm_time_parse_sec(time_str, &(a_time->seconds)) == FALSE) {
 837             return FALSE;
 838         }
 839         offset_s = strstr(time_str, "Z");
 840         if (offset_s == NULL) {
 841             offset_s = strstr(time_str, " ");
 842             if (offset_s) {
 843                 while (isspace(offset_s[0])) {
 844                     offset_s++;
 845                 }
 846             }
 847         }
 848     }
 849 
 850     if (crm_time_parse_offset(offset_s, &(a_time->offset)) == FALSE) {
 851         return FALSE;
 852     }
 853     crm_time_get_sec(a_time->offset, &h, &m, &s);
 854     crm_trace("Got tz: %c%2." PRIu32 ":%.2" PRIu32,
 855               (a_time->offset < 0)? '-' : '+', h, m);
 856 
 857     if (a_time->seconds == DAY_SECONDS) {
 858         // 24:00:00 == 00:00:00 of next day
 859         a_time->seconds = 0;
 860         crm_time_add_days(a_time, 1);
 861     }
 862     return TRUE;
 863 }
 864 
 865 /*
 866  * \internal
 867  * \brief Parse a time object from an ISO 8601 date/time specification
 868  *
 869  * \param[in] date_str  ISO 8601 date/time specification (or
 870  *                      \c PCMK__VALUE_EPOCH)
 871  *
 872  * \return New time object on success, NULL (and set errno) otherwise
 873  */
 874 static crm_time_t *
 875 parse_date(const char *date_str)
     /* [previous][next][first][last][top][bottom][index][help] */
 876 {
 877     const char *time_s = NULL;
 878     crm_time_t *dt = NULL;
 879 
 880     int year = 0;
 881     int month = 0;
 882     int week = 0;
 883     int day = 0;
 884     int rc = 0;
 885 
 886     if (pcmk__str_empty(date_str)) {
 887         crm_err("No ISO 8601 date/time specification given");
 888         goto invalid;
 889     }
 890 
 891     if ((date_str[0] == 'T')
 892         || ((strlen(date_str) > 2) && (date_str[2] == ':'))) {
 893         /* Just a time supplied - Infer current date */
 894         dt = crm_time_new(NULL);
 895         if (date_str[0] == 'T') {
 896             time_s = date_str + 1;
 897         } else {
 898             time_s = date_str;
 899         }
 900         goto parse_time;
 901     }
 902 
 903     dt = crm_time_new_undefined();
 904 
 905     if ((strncasecmp(PCMK__VALUE_EPOCH, date_str, 5) == 0)
 906         && ((date_str[5] == '\0')
 907             || (date_str[5] == '/')
 908             || isspace(date_str[5]))) {
 909         dt->days = 1;
 910         dt->years = 1970;
 911         crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
 912         return dt;
 913     }
 914 
 915     /* YYYY-MM-DD */
 916     rc = sscanf(date_str, "%d-%d-%d", &year, &month, &day);
 917     if (rc == 1) {
 918         /* YYYYMMDD */
 919         rc = sscanf(date_str, "%4d%2d%2d", &year, &month, &day);
 920     }
 921     if (rc == 3) {
 922         if (month > 12) {
 923             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 924                     "because '%d' is not a valid month", date_str, month);
 925             goto invalid;
 926         } else if (day > crm_time_days_in_month(month, year)) {
 927             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 928                     "because '%d' is not a valid day of the month",
 929                     date_str, day);
 930             goto invalid;
 931         } else {
 932             dt->years = year;
 933             dt->days = get_ordinal_days(year, month, day);
 934             crm_trace("Parsed Gregorian date '%.4d-%.3d' from date string '%s'",
 935                       year, dt->days, date_str);
 936         }
 937         goto parse_time;
 938     }
 939 
 940     /* YYYY-DDD */
 941     rc = sscanf(date_str, "%d-%d", &year, &day);
 942     if (rc == 2) {
 943         if (day > year_days(year)) {
 944             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 945                     "because '%d' is not a valid day of the year (max %d)",
 946                     date_str, day, year_days(year));
 947             goto invalid;
 948         }
 949         crm_trace("Parsed ordinal year %d and days %d from date string '%s'",
 950                   year, day, date_str);
 951         dt->days = day;
 952         dt->years = year;
 953         goto parse_time;
 954     }
 955 
 956     /* YYYY-Www-D */
 957     rc = sscanf(date_str, "%d-W%d-%d", &year, &week, &day);
 958     if (rc == 3) {
 959         if (week > crm_time_weeks_in_year(year)) {
 960             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 961                     "because '%d' is not a valid week of the year (max %d)",
 962                     date_str, week, crm_time_weeks_in_year(year));
 963             goto invalid;
 964         } else if (day < 1 || day > 7) {
 965             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 966                     "because '%d' is not a valid day of the week",
 967                     date_str, day);
 968             goto invalid;
 969         } else {
 970             /*
 971              * See https://en.wikipedia.org/wiki/ISO_week_date
 972              *
 973              * Monday 29 December 2008 is written "2009-W01-1"
 974              * Sunday 3 January 2010 is written "2009-W53-7"
 975              * Saturday 27 September 2008 is written "2008-W37-6"
 976              *
 977              * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in week 01.
 978              * If 1 January is on a Friday, Saturday or Sunday, it is in week 52 or 53 of the previous year.
 979              */
 980             int jan1 = crm_time_january1_weekday(year);
 981 
 982             crm_trace("Got year %d (Jan 1 = %d), week %d, and day %d from date string '%s'",
 983                       year, jan1, week, day, date_str);
 984 
 985             dt->years = year;
 986             crm_time_add_days(dt, (week - 1) * 7);
 987 
 988             if (jan1 <= 4) {
 989                 crm_time_add_days(dt, 1 - jan1);
 990             } else {
 991                 crm_time_add_days(dt, 8 - jan1);
 992             }
 993 
 994             crm_time_add_days(dt, day);
 995         }
 996         goto parse_time;
 997     }
 998 
 999     crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str);
1000     goto invalid;
1001 
1002   parse_time:
1003 
1004     if (time_s == NULL) {
1005         time_s = date_str + strspn(date_str, "0123456789-W");
1006         if ((time_s[0] == ' ') || (time_s[0] == 'T')) {
1007             ++time_s;
1008         } else {
1009             time_s = NULL;
1010         }
1011     }
1012     if ((time_s != NULL) && (crm_time_parse(time_s, dt) == FALSE)) {
1013         goto invalid;
1014     }
1015 
1016     crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
1017     if (crm_time_check(dt) == FALSE) {
1018         crm_err("'%s' is not a valid ISO 8601 date/time specification",
1019                 date_str);
1020         goto invalid;
1021     }
1022     return dt;
1023 
1024 invalid:
1025     crm_time_free(dt);
1026     errno = EINVAL;
1027     return NULL;
1028 }
1029 
1030 // Parse an ISO 8601 numeric value and return number of characters consumed
1031 static int
1032 parse_int(const char *str, int *result)
     /* [previous][next][first][last][top][bottom][index][help] */
1033 {
1034     unsigned int lpc;
1035     int offset = (str[0] == 'T')? 1 : 0;
1036     bool negate = false;
1037 
1038     *result = 0;
1039 
1040     // @TODO This cannot handle combinations of these characters
1041     switch (str[offset]) {
1042         case '.':
1043         case ',':
1044             return 0; // Fractions are not supported
1045 
1046         case '-':
1047             negate = true;
1048             offset++;
1049             break;
1050 
1051         case '+':
1052         case ':':
1053             offset++;
1054             break;
1055 
1056         default:
1057             break;
1058     }
1059 
1060     for (lpc = 0; (lpc < 10) && isdigit(str[offset]); lpc++) {
1061         const int digit = str[offset++] - '0';
1062 
1063         if ((*result * 10LL + digit) > INT_MAX) {
1064             return 0; // Overflow
1065         }
1066         *result = *result * 10 + digit;
1067     }
1068     if (negate) {
1069         *result = 0 - *result;
1070     }
1071     return (lpc > 0)? offset : 0;
1072 }
1073 
1074 /*!
1075  * \brief Parse a time duration from an ISO 8601 duration specification
1076  *
1077  * \param[in] period_s  ISO 8601 duration specification (optionally followed by
1078  *                      whitespace, after which the rest of the string will be
1079  *                      ignored)
1080  *
1081  * \return New time object on success, NULL (and set errno) otherwise
1082  * \note It is the caller's responsibility to return the result using
1083  *       crm_time_free().
1084  */
1085 crm_time_t *
1086 crm_time_parse_duration(const char *period_s)
     /* [previous][next][first][last][top][bottom][index][help] */
1087 {
1088     gboolean is_time = FALSE;
1089     crm_time_t *diff = NULL;
1090 
1091     if (pcmk__str_empty(period_s)) {
1092         crm_err("No ISO 8601 time duration given");
1093         goto invalid;
1094     }
1095     if (period_s[0] != 'P') {
1096         crm_err("'%s' is not a valid ISO 8601 time duration "
1097                 "because it does not start with a 'P'", period_s);
1098         goto invalid;
1099     }
1100     if ((period_s[1] == '\0') || isspace(period_s[1])) {
1101         crm_err("'%s' is not a valid ISO 8601 time duration "
1102                 "because nothing follows 'P'", period_s);
1103         goto invalid;
1104     }
1105 
1106     diff = crm_time_new_undefined();
1107 
1108     for (const char *current = period_s + 1;
1109          current[0] && (current[0] != '/') && !isspace(current[0]);
1110          ++current) {
1111 
1112         int an_int = 0, rc;
1113 
1114         if (current[0] == 'T') {
1115             /* A 'T' separates year/month/day from hour/minute/seconds. We don't
1116              * require it strictly, but just use it to differentiate month from
1117              * minutes.
1118              */
1119             is_time = TRUE;
1120             continue;
1121         }
1122 
1123         // An integer must be next
1124         rc = parse_int(current, &an_int);
1125         if (rc == 0) {
1126             crm_err("'%s' is not a valid ISO 8601 time duration "
1127                     "because no valid integer at '%s'", period_s, current);
1128             goto invalid;
1129         }
1130         current += rc;
1131 
1132         // A time unit must be next (we're not strict about the order)
1133         switch (current[0]) {
1134             case 'Y':
1135                 diff->years = an_int;
1136                 break;
1137 
1138             case 'M':
1139                 if (!is_time) { // Months
1140                     diff->months = an_int;
1141 
1142                 // Minutes
1143                 } else if ((diff->seconds + (an_int * 60LL)) > INT_MAX) {
1144                     crm_err("'%s' is not a valid ISO 8601 time duration "
1145                             "because integer at '%s' is too large",
1146                             period_s, current - rc);
1147                     goto invalid;
1148                 } else {
1149                     diff->seconds += an_int * 60;
1150                 }
1151                 break;
1152 
1153             case 'W':
1154                 if ((diff->days + (an_int * 7LL)) > INT_MAX) {
1155                     crm_err("'%s' is not a valid ISO 8601 time duration "
1156                             "because integer at '%s' is too large",
1157                             period_s, current - rc);
1158                     goto invalid;
1159                 } else {
1160                     diff->days += an_int * 7;
1161                 }
1162                 break;
1163 
1164             case 'D':
1165                 if ((diff->days + (long long) an_int) > INT_MAX) {
1166                     crm_err("'%s' is not a valid ISO 8601 time duration "
1167                             "because integer at '%s' is too large",
1168                             period_s, current - rc);
1169                     goto invalid;
1170                 } else {
1171                     diff->days += an_int;
1172                 }
1173                 break;
1174 
1175             case 'H':
1176                 if ((diff->seconds + ((long long) an_int * HOUR_SECONDS))
1177                     > INT_MAX) {
1178                     crm_err("'%s' is not a valid ISO 8601 time duration "
1179                             "because integer at '%s' is too large",
1180                             period_s, current - rc);
1181                     goto invalid;
1182                 } else {
1183                     diff->seconds += an_int * HOUR_SECONDS;
1184                 }
1185                 break;
1186 
1187             case 'S':
1188                 if ((diff->seconds + (long long) an_int) > INT_MAX) {
1189                     crm_err("'%s' is not a valid ISO 8601 time duration "
1190                             "because integer at '%s' is too large",
1191                             period_s, current - rc);
1192                     goto invalid;
1193                 } else {
1194                     diff->seconds += an_int;
1195                 }
1196                 break;
1197 
1198             case '\0':
1199                 crm_err("'%s' is not a valid ISO 8601 time duration "
1200                         "because no units after %d", period_s, an_int);
1201                 goto invalid;
1202 
1203             default:
1204                 crm_err("'%s' is not a valid ISO 8601 time duration "
1205                         "because '%c' is not a valid time unit",
1206                         period_s, current[0]);
1207                 goto invalid;
1208         }
1209     }
1210 
1211     if (!crm_time_is_defined(diff)) {
1212         crm_err("'%s' is not a valid ISO 8601 time duration "
1213                 "because no amounts and units given", period_s);
1214         goto invalid;
1215     }
1216 
1217     diff->duration = TRUE;
1218     return diff;
1219 
1220 invalid:
1221     crm_time_free(diff);
1222     errno = EINVAL;
1223     return NULL;
1224 }
1225 
1226 /*!
1227  * \brief Parse a time period from an ISO 8601 interval specification
1228  *
1229  * \param[in] period_str  ISO 8601 interval specification (start/end,
1230  *                        start/duration, or duration/end)
1231  *
1232  * \return New time period object on success, NULL (and set errno) otherwise
1233  * \note The caller is responsible for freeing the result using
1234  *       crm_time_free_period().
1235  */
1236 crm_time_period_t *
1237 crm_time_parse_period(const char *period_str)
     /* [previous][next][first][last][top][bottom][index][help] */
1238 {
1239     const char *original = period_str;
1240     crm_time_period_t *period = NULL;
1241 
1242     if (pcmk__str_empty(period_str)) {
1243         crm_err("No ISO 8601 time period given");
1244         goto invalid;
1245     }
1246 
1247     tzset();
1248     period = pcmk__assert_alloc(1, sizeof(crm_time_period_t));
1249 
1250     if (period_str[0] == 'P') {
1251         period->diff = crm_time_parse_duration(period_str);
1252         if (period->diff == NULL) {
1253             goto error;
1254         }
1255     } else {
1256         period->start = parse_date(period_str);
1257         if (period->start == NULL) {
1258             goto error;
1259         }
1260     }
1261 
1262     period_str = strstr(original, "/");
1263     if (period_str) {
1264         ++period_str;
1265         if (period_str[0] == 'P') {
1266             if (period->diff != NULL) {
1267                 crm_err("'%s' is not a valid ISO 8601 time period "
1268                         "because it has two durations",
1269                         original);
1270                 goto invalid;
1271             }
1272             period->diff = crm_time_parse_duration(period_str);
1273             if (period->diff == NULL) {
1274                 goto error;
1275             }
1276         } else {
1277             period->end = parse_date(period_str);
1278             if (period->end == NULL) {
1279                 goto error;
1280             }
1281         }
1282 
1283     } else if (period->diff != NULL) {
1284         // Only duration given, assume start is now
1285         period->start = crm_time_new(NULL);
1286 
1287     } else {
1288         // Only start given
1289         crm_err("'%s' is not a valid ISO 8601 time period "
1290                 "because it has no duration or ending time",
1291                 original);
1292         goto invalid;
1293     }
1294 
1295     if (period->start == NULL) {
1296         period->start = crm_time_subtract(period->end, period->diff);
1297 
1298     } else if (period->end == NULL) {
1299         period->end = crm_time_add(period->start, period->diff);
1300     }
1301 
1302     if (crm_time_check(period->start) == FALSE) {
1303         crm_err("'%s' is not a valid ISO 8601 time period "
1304                 "because the start is invalid", period_str);
1305         goto invalid;
1306     }
1307     if (crm_time_check(period->end) == FALSE) {
1308         crm_err("'%s' is not a valid ISO 8601 time period "
1309                 "because the end is invalid", period_str);
1310         goto invalid;
1311     }
1312     return period;
1313 
1314 invalid:
1315     errno = EINVAL;
1316 error:
1317     crm_time_free_period(period);
1318     return NULL;
1319 }
1320 
1321 /*!
1322  * \brief Free a dynamically allocated time period object
1323  *
1324  * \param[in,out] period  Time period to free
1325  */
1326 void
1327 crm_time_free_period(crm_time_period_t *period)
     /* [previous][next][first][last][top][bottom][index][help] */
1328 {
1329     if (period) {
1330         crm_time_free(period->start);
1331         crm_time_free(period->end);
1332         crm_time_free(period->diff);
1333         free(period);
1334     }
1335 }
1336 
1337 void
1338 crm_time_set(crm_time_t *target, const crm_time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1339 {
1340     crm_trace("target=%p, source=%p", target, source);
1341 
1342     CRM_CHECK(target != NULL && source != NULL, return);
1343 
1344     target->years = source->years;
1345     target->days = source->days;
1346     target->months = source->months;    /* Only for durations */
1347     target->seconds = source->seconds;
1348     target->offset = source->offset;
1349 
1350     crm_time_log(LOG_TRACE, "source", source,
1351                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
1352     crm_time_log(LOG_TRACE, "target", target,
1353                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
1354 }
1355 
1356 static void
1357 ha_set_tm_time(crm_time_t *target, const struct tm *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1358 {
1359     int h_offset = 0;
1360     int m_offset = 0;
1361 
1362     /* Ensure target is fully initialized */
1363     target->years = 0;
1364     target->months = 0;
1365     target->days = 0;
1366     target->seconds = 0;
1367     target->offset = 0;
1368     target->duration = FALSE;
1369 
1370     if (source->tm_year > 0) {
1371         /* years since 1900 */
1372         target->years = 1900 + source->tm_year;
1373     }
1374 
1375     if (source->tm_yday >= 0) {
1376         /* days since January 1 [0-365] */
1377         target->days = 1 + source->tm_yday;
1378     }
1379 
1380     if (source->tm_hour >= 0) {
1381         target->seconds += HOUR_SECONDS * source->tm_hour;
1382     }
1383     if (source->tm_min >= 0) {
1384         target->seconds += 60 * source->tm_min;
1385     }
1386     if (source->tm_sec >= 0) {
1387         target->seconds += source->tm_sec;
1388     }
1389 
1390     /* tm_gmtoff == offset from UTC in seconds */
1391     h_offset = GMTOFF(source) / HOUR_SECONDS;
1392     m_offset = (GMTOFF(source) - (HOUR_SECONDS * h_offset)) / 60;
1393     crm_trace("Time offset is %lds (%.2d:%.2d)",
1394               GMTOFF(source), h_offset, m_offset);
1395 
1396     target->offset += HOUR_SECONDS * h_offset;
1397     target->offset += 60 * m_offset;
1398 }
1399 
1400 void
1401 crm_time_set_timet(crm_time_t *target, const time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1402 {
1403     ha_set_tm_time(target, localtime(source));
1404 }
1405 
1406 /*!
1407  * \internal
1408  * \brief Set one time object to another if the other is earlier
1409  *
1410  * \param[in,out] target  Time object to set
1411  * \param[in]     source  Time object to use if earlier
1412  */
1413 void
1414 pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1415 {
1416     if ((target != NULL) && (source != NULL)
1417         && (!crm_time_is_defined(target)
1418             || (crm_time_compare(source, target) < 0))) {
1419         crm_time_set(target, source);
1420     }
1421 }
1422 
1423 crm_time_t *
1424 pcmk_copy_time(const crm_time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1425 {
1426     crm_time_t *target = crm_time_new_undefined();
1427 
1428     crm_time_set(target, source);
1429     return target;
1430 }
1431 
1432 /*!
1433  * \internal
1434  * \brief Convert a \p time_t time to a \p crm_time_t time
1435  *
1436  * \param[in] source  Time to convert
1437  *
1438  * \return A \p crm_time_t object representing \p source
1439  */
1440 crm_time_t *
1441 pcmk__copy_timet(time_t source)
     /* [previous][next][first][last][top][bottom][index][help] */
1442 {
1443     crm_time_t *target = crm_time_new_undefined();
1444 
1445     crm_time_set_timet(target, &source);
1446     return target;
1447 }
1448 
1449 crm_time_t *
1450 crm_time_add(const crm_time_t *dt, const crm_time_t *value)
     /* [previous][next][first][last][top][bottom][index][help] */
1451 {
1452     crm_time_t *utc = NULL;
1453     crm_time_t *answer = NULL;
1454 
1455     if ((dt == NULL) || (value == NULL)) {
1456         errno = EINVAL;
1457         return NULL;
1458     }
1459 
1460     answer = pcmk_copy_time(dt);
1461 
1462     utc = crm_get_utc_time(value);
1463     if (utc == NULL) {
1464         crm_time_free(answer);
1465         return NULL;
1466     }
1467 
1468     answer->years += utc->years;
1469     crm_time_add_months(answer, utc->months);
1470     crm_time_add_days(answer, utc->days);
1471     crm_time_add_seconds(answer, utc->seconds);
1472 
1473     crm_time_free(utc);
1474     return answer;
1475 }
1476 
1477 /*!
1478  * \internal
1479  * \brief Return the XML attribute name corresponding to a time component
1480  *
1481  * \param[in] component  Component to check
1482  *
1483  * \return XML attribute name corresponding to \p component, or NULL if
1484  *         \p component is invalid
1485  */
1486 const char *
1487 pcmk__time_component_attr(enum pcmk__time_component component)
     /* [previous][next][first][last][top][bottom][index][help] */
1488 {
1489     switch (component) {
1490         case pcmk__time_years:
1491             return PCMK_XA_YEARS;
1492 
1493         case pcmk__time_months:
1494             return PCMK_XA_MONTHS;
1495 
1496         case pcmk__time_weeks:
1497             return PCMK_XA_WEEKS;
1498 
1499         case pcmk__time_days:
1500             return PCMK_XA_DAYS;
1501 
1502         case pcmk__time_hours:
1503             return PCMK_XA_HOURS;
1504 
1505         case pcmk__time_minutes:
1506             return PCMK_XA_MINUTES;
1507 
1508         case pcmk__time_seconds:
1509             return PCMK_XA_SECONDS;
1510 
1511         default:
1512             return NULL;
1513     }
1514 }
1515 
1516 typedef void (*component_fn_t)(crm_time_t *, int);
1517 
1518 /*!
1519  * \internal
1520  * \brief Get the addition function corresponding to a time component
1521  * \param[in] component  Component to check
1522  *
1523  * \return Addition function corresponding to \p component, or NULL if
1524  *         \p component is invalid
1525  */
1526 static component_fn_t
1527 component_fn(enum pcmk__time_component component)
     /* [previous][next][first][last][top][bottom][index][help] */
1528 {
1529     switch (component) {
1530         case pcmk__time_years:
1531             return crm_time_add_years;
1532 
1533         case pcmk__time_months:
1534             return crm_time_add_months;
1535 
1536         case pcmk__time_weeks:
1537             return crm_time_add_weeks;
1538 
1539         case pcmk__time_days:
1540             return crm_time_add_days;
1541 
1542         case pcmk__time_hours:
1543             return crm_time_add_hours;
1544 
1545         case pcmk__time_minutes:
1546             return crm_time_add_minutes;
1547 
1548         case pcmk__time_seconds:
1549             return crm_time_add_seconds;
1550 
1551         default:
1552             return NULL;
1553     }
1554 
1555 }
1556 
1557 /*!
1558  * \internal
1559  * \brief Add the value of an XML attribute to a time object
1560  *
1561  * \param[in,out] t          Time object to add to
1562  * \param[in]     component  Component of \p t to add to
1563  * \param[in]     xml        XML with value to add
1564  *
1565  * \return Standard Pacemaker return code
1566  */
1567 int
1568 pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component,
     /* [previous][next][first][last][top][bottom][index][help] */
1569                         const xmlNode *xml)
1570 {
1571     long long value;
1572     const char *attr = pcmk__time_component_attr(component);
1573     component_fn_t add = component_fn(component);
1574 
1575     if ((t == NULL) || (attr == NULL) || (add == NULL)) {
1576         return EINVAL;
1577     }
1578 
1579     if (xml == NULL) {
1580         return pcmk_rc_ok;
1581     }
1582 
1583     if (pcmk__scan_ll(crm_element_value(xml, attr), &value,
1584                       0LL) != pcmk_rc_ok) {
1585         return pcmk_rc_unpack_error;
1586     }
1587 
1588     if ((value < INT_MIN) || (value > INT_MAX)) {
1589         return ERANGE;
1590     }
1591 
1592     if (value != 0LL) {
1593         add(t, (int) value);
1594     }
1595     return pcmk_rc_ok;
1596 }
1597 
1598 crm_time_t *
1599 crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value)
     /* [previous][next][first][last][top][bottom][index][help] */
1600 {
1601     crm_time_t *utc = NULL;
1602     crm_time_t *answer = NULL;
1603 
1604     if ((dt == NULL) || (value == NULL)) {
1605         errno = EINVAL;
1606         return NULL;
1607     }
1608 
1609     utc = crm_get_utc_time(value);
1610     if (utc == NULL) {
1611         return NULL;
1612     }
1613 
1614     answer = crm_get_utc_time(dt);
1615     if (answer == NULL) {
1616         crm_time_free(utc);
1617         return NULL;
1618     }
1619     answer->duration = TRUE;
1620 
1621     answer->years -= utc->years;
1622     if(utc->months != 0) {
1623         crm_time_add_months(answer, -utc->months);
1624     }
1625     crm_time_add_days(answer, -utc->days);
1626     crm_time_add_seconds(answer, -utc->seconds);
1627 
1628     crm_time_free(utc);
1629     return answer;
1630 }
1631 
1632 crm_time_t *
1633 crm_time_subtract(const crm_time_t *dt, const crm_time_t *value)
     /* [previous][next][first][last][top][bottom][index][help] */
1634 {
1635     crm_time_t *utc = NULL;
1636     crm_time_t *answer = NULL;
1637 
1638     if ((dt == NULL) || (value == NULL)) {
1639         errno = EINVAL;
1640         return NULL;
1641     }
1642 
1643     utc = crm_get_utc_time(value);
1644     if (utc == NULL) {
1645         return NULL;
1646     }
1647 
1648     answer = pcmk_copy_time(dt);
1649     answer->years -= utc->years;
1650     if(utc->months != 0) {
1651         crm_time_add_months(answer, -utc->months);
1652     }
1653     crm_time_add_days(answer, -utc->days);
1654     crm_time_add_seconds(answer, -utc->seconds);
1655     crm_time_free(utc);
1656 
1657     return answer;
1658 }
1659 
1660 /*!
1661  * \brief Check whether a time object represents a sensible date/time
1662  *
1663  * \param[in] dt  Date/time object to check
1664  *
1665  * \return \c true if years, days, and seconds are sensible, \c false otherwise
1666  */
1667 bool
1668 crm_time_check(const crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1669 {
1670     return (dt != NULL)
1671            && (dt->days > 0) && (dt->days <= year_days(dt->years))
1672            && (dt->seconds >= 0) && (dt->seconds < DAY_SECONDS);
1673 }
1674 
1675 #define do_cmp_field(l, r, field)                                       \
1676     if(rc == 0) {                                                       \
1677                 if(l->field > r->field) {                               \
1678                         crm_trace("%s: %d > %d",                        \
1679                                     #field, l->field, r->field);        \
1680                         rc = 1;                                         \
1681                 } else if(l->field < r->field) {                        \
1682                         crm_trace("%s: %d < %d",                        \
1683                                     #field, l->field, r->field);        \
1684                         rc = -1;                                        \
1685                 }                                                       \
1686     }
1687 
1688 int
1689 crm_time_compare(const crm_time_t *a, const crm_time_t *b)
     /* [previous][next][first][last][top][bottom][index][help] */
1690 {
1691     int rc = 0;
1692     crm_time_t *t1 = crm_get_utc_time(a);
1693     crm_time_t *t2 = crm_get_utc_time(b);
1694 
1695     if ((t1 == NULL) && (t2 == NULL)) {
1696         rc = 0;
1697     } else if (t1 == NULL) {
1698         rc = -1;
1699     } else if (t2 == NULL) {
1700         rc = 1;
1701     } else {
1702         do_cmp_field(t1, t2, years);
1703         do_cmp_field(t1, t2, days);
1704         do_cmp_field(t1, t2, seconds);
1705     }
1706 
1707     crm_time_free(t1);
1708     crm_time_free(t2);
1709     return rc;
1710 }
1711 
1712 /*!
1713  * \brief Add a given number of seconds to a date/time or duration
1714  *
1715  * \param[in,out] a_time  Date/time or duration to add seconds to
1716  * \param[in]     extra   Number of seconds to add
1717  */
1718 void
1719 crm_time_add_seconds(crm_time_t *a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1720 {
1721     int days = 0;
1722 
1723     crm_trace("Adding %d seconds to %d (max=%d)",
1724               extra, a_time->seconds, DAY_SECONDS);
1725     a_time->seconds += extra;
1726     days = a_time->seconds / DAY_SECONDS;
1727     a_time->seconds %= DAY_SECONDS;
1728 
1729     // Don't have negative seconds
1730     if (a_time->seconds < 0) {
1731         a_time->seconds += DAY_SECONDS;
1732         --days;
1733     }
1734 
1735     crm_time_add_days(a_time, days);
1736 }
1737 
1738 void
1739 crm_time_add_days(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1740 {
1741     int lower_bound = 1;
1742     int ydays = crm_time_leapyear(a_time->years) ? 366 : 365;
1743 
1744     crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days);
1745 
1746     a_time->days += extra;
1747     while (a_time->days > ydays) {
1748         a_time->years++;
1749         a_time->days -= ydays;
1750         ydays = crm_time_leapyear(a_time->years) ? 366 : 365;
1751     }
1752 
1753     if(a_time->duration) {
1754         lower_bound = 0;
1755     }
1756 
1757     while (a_time->days < lower_bound) {
1758         a_time->years--;
1759         a_time->days += crm_time_leapyear(a_time->years) ? 366 : 365;
1760     }
1761 }
1762 
1763 void
1764 crm_time_add_months(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1765 {
1766     int lpc;
1767     uint32_t y, m, d, dmax;
1768 
1769     crm_time_get_gregorian(a_time, &y, &m, &d);
1770     crm_trace("Adding %d months to %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32,
1771               extra, y, m, d);
1772 
1773     if (extra > 0) {
1774         for (lpc = extra; lpc > 0; lpc--) {
1775             m++;
1776             if (m == 13) {
1777                 m = 1;
1778                 y++;
1779             }
1780         }
1781     } else {
1782         for (lpc = -extra; lpc > 0; lpc--) {
1783             m--;
1784             if (m == 0) {
1785                 m = 12;
1786                 y--;
1787             }
1788         }
1789     }
1790 
1791     dmax = crm_time_days_in_month(m, y);
1792     if (dmax < d) {
1793         /* Preserve day-of-month unless the month doesn't have enough days */
1794         d = dmax;
1795     }
1796 
1797     crm_trace("Calculated %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d);
1798 
1799     a_time->years = y;
1800     a_time->days = get_ordinal_days(y, m, d);
1801 
1802     crm_time_get_gregorian(a_time, &y, &m, &d);
1803     crm_trace("Got %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d);
1804 }
1805 
1806 void
1807 crm_time_add_minutes(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1808 {
1809     crm_time_add_seconds(a_time, extra * 60);
1810 }
1811 
1812 void
1813 crm_time_add_hours(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1814 {
1815     crm_time_add_seconds(a_time, extra * HOUR_SECONDS);
1816 }
1817 
1818 void
1819 crm_time_add_weeks(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1820 {
1821     crm_time_add_days(a_time, extra * 7);
1822 }
1823 
1824 void
1825 crm_time_add_years(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1826 {
1827     a_time->years += extra;
1828 }
1829 
1830 static void
1831 ha_get_tm_time(struct tm *target, const crm_time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1832 {
1833     *target = (struct tm) {
1834         .tm_year = source->years - 1900,
1835         .tm_mday = source->days,
1836         .tm_sec = source->seconds % 60,
1837         .tm_min = ( source->seconds / 60 ) % 60,
1838         .tm_hour = source->seconds / HOUR_SECONDS,
1839         .tm_isdst = -1, /* don't adjust */
1840 
1841 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
1842         .tm_gmtoff = source->offset
1843 #endif
1844     };
1845     mktime(target);
1846 }
1847 
1848 /* The high-resolution variant of time object was added to meet an immediate
1849  * need, and is kept internal API.
1850  *
1851  * @TODO The long-term goal is to come up with a clean, unified design for a
1852  *       time type (or types) that meets all the various needs, to replace
1853  *       crm_time_t, pcmk__time_hr_t, and struct timespec (in lrmd_cmd_t).
1854  *       Using glib's GDateTime is a possibility (if we are willing to require
1855  *       glib >= 2.26).
1856  */
1857 
1858 pcmk__time_hr_t *
1859 pcmk__time_hr_convert(pcmk__time_hr_t *target, const crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1860 {
1861     pcmk__time_hr_t *hr_dt = NULL;
1862 
1863     if (dt) {
1864         hr_dt = target;
1865         if (hr_dt == NULL) {
1866             hr_dt = pcmk__assert_alloc(1, sizeof(pcmk__time_hr_t));
1867         }
1868 
1869         *hr_dt = (pcmk__time_hr_t) {
1870             .years = dt->years,
1871             .months = dt->months,
1872             .days = dt->days,
1873             .seconds = dt->seconds,
1874             .offset = dt->offset,
1875             .duration = dt->duration
1876         };
1877     }
1878 
1879     return hr_dt;
1880 }
1881 
1882 void
1883 pcmk__time_set_hr_dt(crm_time_t *target, const pcmk__time_hr_t *hr_dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1884 {
1885     CRM_ASSERT((hr_dt) && (target));
1886     *target = (crm_time_t) {
1887         .years = hr_dt->years,
1888         .months = hr_dt->months,
1889         .days = hr_dt->days,
1890         .seconds = hr_dt->seconds,
1891         .offset = hr_dt->offset,
1892         .duration = hr_dt->duration
1893     };
1894 }
1895 
1896 /*!
1897  * \internal
1898  * \brief Return the current time as a high-resolution time
1899  *
1900  * \param[out] epoch  If not NULL, this will be set to seconds since epoch
1901  *
1902  * \return Newly allocated high-resolution time set to the current time
1903  */
1904 pcmk__time_hr_t *
1905 pcmk__time_hr_now(time_t *epoch)
     /* [previous][next][first][last][top][bottom][index][help] */
1906 {
1907     struct timespec tv;
1908     crm_time_t dt;
1909     pcmk__time_hr_t *hr;
1910 
1911     qb_util_timespec_from_epoch_get(&tv);
1912     if (epoch != NULL) {
1913         *epoch = tv.tv_sec;
1914     }
1915     crm_time_set_timet(&dt, &(tv.tv_sec));
1916     hr = pcmk__time_hr_convert(NULL, &dt);
1917     if (hr != NULL) {
1918         hr->useconds = tv.tv_nsec / QB_TIME_NS_IN_USEC;
1919     }
1920     return hr;
1921 }
1922 
1923 pcmk__time_hr_t *
1924 pcmk__time_hr_new(const char *date_time)
     /* [previous][next][first][last][top][bottom][index][help] */
1925 {
1926     pcmk__time_hr_t *hr_dt = NULL;
1927 
1928     if (date_time == NULL) {
1929         hr_dt = pcmk__time_hr_now(NULL);
1930     } else {
1931         crm_time_t *dt;
1932 
1933         dt = parse_date(date_time);
1934         hr_dt = pcmk__time_hr_convert(NULL, dt);
1935         crm_time_free(dt);
1936     }
1937     return hr_dt;
1938 }
1939 
1940 void
1941 pcmk__time_hr_free(pcmk__time_hr_t * hr_dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1942 {
1943     free(hr_dt);
1944 }
1945 
1946 /*!
1947  * \internal
1948  * \brief Expand a date/time format string, including %N for nanoseconds
1949  *
1950  * \param[in] format  Date/time format string as per strftime(3) with the
1951  *                    addition of %N for nanoseconds
1952  * \param[in] hr_dt   Time value to format
1953  *
1954  * \return Newly allocated string with formatted string
1955  */
1956 char *
1957 pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1958 {
1959     int scanned_pos = 0; // How many characters of format have been parsed
1960     int printed_pos = 0; // How many characters of format have been processed
1961     size_t date_len = 0;
1962 
1963     char nano_s[10] = { '\0', };
1964     char date_s[128] = { '\0', };
1965 
1966     struct tm tm = { 0, };
1967     crm_time_t dt = { 0, };
1968 
1969     if (format == NULL) {
1970         return NULL;
1971     }
1972     pcmk__time_set_hr_dt(&dt, hr_dt);
1973     ha_get_tm_time(&tm, &dt);
1974     sprintf(nano_s, "%06d000", hr_dt->useconds);
1975 
1976     while (format[scanned_pos] != '\0') {
1977         int fmt_pos;            // Index after last character to pass as-is
1978         int nano_digits = 0;    // Length of %N field width (if any)
1979         char *tmp_fmt_s = NULL;
1980         size_t nbytes = 0;
1981 
1982         // Look for next format specifier
1983         const char *mark_s = strchr(&format[scanned_pos], '%');
1984 
1985         if (mark_s == NULL) {
1986             // No more specifiers, so pass remaining string to strftime() as-is
1987             scanned_pos = strlen(format);
1988             fmt_pos = scanned_pos;
1989 
1990         } else {
1991             fmt_pos = mark_s - format; // Index of %
1992 
1993             // Skip % and any field width
1994             scanned_pos = fmt_pos + 1;
1995             while (isdigit(format[scanned_pos])) {
1996                 scanned_pos++;
1997             }
1998 
1999             switch (format[scanned_pos]) {
2000                 case '\0': // Literal % and possibly digits at end of string
2001                     fmt_pos = scanned_pos; // Pass remaining string as-is
2002                     break;
2003 
2004                 case 'N': // %[width]N
2005                     scanned_pos++;
2006 
2007                     // Parse field width
2008                     nano_digits = atoi(&format[fmt_pos + 1]);
2009                     nano_digits = QB_MAX(nano_digits, 0);
2010                     nano_digits = QB_MIN(nano_digits, 6);
2011                     break;
2012 
2013                 default: // Some other specifier
2014                     if (format[++scanned_pos] != '\0') { // More to parse
2015                         continue;
2016                     }
2017                     fmt_pos = scanned_pos; // Pass remaining string as-is
2018                     break;
2019             }
2020         }
2021 
2022         if (date_len >= sizeof(date_s)) {
2023             return NULL; // No room for remaining string
2024         }
2025 
2026         tmp_fmt_s = strndup(&format[printed_pos], fmt_pos - printed_pos);
2027 #ifdef HAVE_FORMAT_NONLITERAL
2028 #pragma GCC diagnostic push
2029 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
2030 #endif
2031         nbytes = strftime(&date_s[date_len], sizeof(date_s) - date_len,
2032                           tmp_fmt_s, &tm);
2033 #ifdef HAVE_FORMAT_NONLITERAL
2034 #pragma GCC diagnostic pop
2035 #endif
2036         free(tmp_fmt_s);
2037         if (nbytes == 0) { // Would overflow buffer
2038             return NULL;
2039         }
2040         date_len += nbytes;
2041         printed_pos = scanned_pos;
2042         if (nano_digits != 0) {
2043             int nc = 0;
2044 
2045             if (date_len >= sizeof(date_s)) {
2046                 return NULL; // No room to add nanoseconds
2047             }
2048             nc = snprintf(&date_s[date_len], sizeof(date_s) - date_len,
2049                           "%.*s", nano_digits, nano_s);
2050 
2051             if ((nc < 0) || (nc == (sizeof(date_s) - date_len))) {
2052                 return NULL; // Error or would overflow buffer
2053             }
2054             date_len += nc;
2055         }
2056     }
2057 
2058     return (date_len == 0)? NULL : pcmk__str_copy(date_s);
2059 }
2060 
2061 /*!
2062  * \internal
2063  * \brief Return a human-friendly string corresponding to an epoch time value
2064  *
2065  * \param[in]  source  Pointer to epoch time value (or \p NULL for current time)
2066  * \param[in]  flags   Group of \p crm_time_* flags controlling display format
2067  *                     (0 to use \p ctime() with newline removed)
2068  *
2069  * \return String representation of \p source on success (may be empty depending
2070  *         on \p flags; guaranteed not to be \p NULL)
2071  *
2072  * \note The caller is responsible for freeing the return value using \p free().
2073  */
2074 char *
2075 pcmk__epoch2str(const time_t *source, uint32_t flags)
     /* [previous][next][first][last][top][bottom][index][help] */
2076 {
2077     time_t epoch_time = (source == NULL)? time(NULL) : *source;
2078 
2079     if (flags == 0) {
2080         return pcmk__str_copy(pcmk__trim(ctime(&epoch_time)));
2081     } else {
2082         crm_time_t dt;
2083 
2084         crm_time_set_timet(&dt, &epoch_time);
2085         return crm_time_as_string(&dt, flags);
2086     }
2087 }
2088 
2089 /*!
2090  * \internal
2091  * \brief Return a human-friendly string corresponding to seconds-and-
2092  *        nanoseconds value
2093  *
2094  * Time is shown with microsecond resolution if \p crm_time_usecs is in \p
2095  * flags.
2096  *
2097  * \param[in]  ts     Time in seconds and nanoseconds (or \p NULL for current
2098  *                    time)
2099  * \param[in]  flags  Group of \p crm_time_* flags controlling display format
2100  *
2101  * \return String representation of \p ts on success (may be empty depending on
2102  *         \p flags; guaranteed not to be \p NULL)
2103  *
2104  * \note The caller is responsible for freeing the return value using \p free().
2105  */
2106 char *
2107 pcmk__timespec2str(const struct timespec *ts, uint32_t flags)
     /* [previous][next][first][last][top][bottom][index][help] */
2108 {
2109     struct timespec tmp_ts;
2110     crm_time_t dt;
2111     char result[DATE_MAX] = { 0 };
2112 
2113     if (ts == NULL) {
2114         qb_util_timespec_from_epoch_get(&tmp_ts);
2115         ts = &tmp_ts;
2116     }
2117     crm_time_set_timet(&dt, &ts->tv_sec);
2118     time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags, result);
2119     return pcmk__str_copy(result);
2120 }
2121 
2122 /*!
2123  * \internal
2124  * \brief Given a millisecond interval, return a log-friendly string
2125  *
2126  * \param[in] interval_ms  Interval in milliseconds
2127  *
2128  * \return Readable version of \p interval_ms
2129  *
2130  * \note The return value is a pointer to static memory that will be
2131  *       overwritten by later calls to this function.
2132  */
2133 const char *
2134 pcmk__readable_interval(guint interval_ms)
     /* [previous][next][first][last][top][bottom][index][help] */
2135 {
2136 #define MS_IN_S (1000)
2137 #define MS_IN_M (MS_IN_S * 60)
2138 #define MS_IN_H (MS_IN_M * 60)
2139 #define MS_IN_D (MS_IN_H * 24)
2140 #define MAXSTR sizeof("..d..h..m..s...ms")
2141     static char str[MAXSTR];
2142     int offset = 0;
2143 
2144     str[0] = '\0';
2145     if (interval_ms >= MS_IN_D) {
2146         offset += snprintf(str + offset, MAXSTR - offset, "%ud",
2147                            interval_ms / MS_IN_D);
2148         interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D;
2149     }
2150     if (interval_ms >= MS_IN_H) {
2151         offset += snprintf(str + offset, MAXSTR - offset, "%uh",
2152                            interval_ms / MS_IN_H);
2153         interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H;
2154     }
2155     if (interval_ms >= MS_IN_M) {
2156         offset += snprintf(str + offset, MAXSTR - offset, "%um",
2157                            interval_ms / MS_IN_M);
2158         interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M;
2159     }
2160 
2161     // Ns, N.NNNs, or NNNms
2162     if (interval_ms >= MS_IN_S) {
2163         offset += snprintf(str + offset, MAXSTR - offset, "%u",
2164                            interval_ms / MS_IN_S);
2165         interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S;
2166         if (interval_ms > 0) {
2167             offset += snprintf(str + offset, MAXSTR - offset, ".%03u",
2168                                interval_ms);
2169         }
2170         (void) snprintf(str + offset, MAXSTR - offset, "s");
2171 
2172     } else if (interval_ms > 0) {
2173         (void) snprintf(str + offset, MAXSTR - offset, "%ums", interval_ms);
2174 
2175     } else if (str[0] == '\0') {
2176         strcpy(str, "0s");
2177     }
2178     return str;
2179 }

/* [previous][next][first][last][top][bottom][index][help] */