Coverage Report

Created: 2026-01-02 06:28

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