Coverage Report

Created: 2026-04-10 06:58

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