diff options
Diffstat (limited to 'lib/timeutils.c')
-rw-r--r-- | lib/timeutils.c | 707 |
1 files changed, 707 insertions, 0 deletions
diff --git a/lib/timeutils.c b/lib/timeutils.c new file mode 100644 index 0000000..34c7c8d --- /dev/null +++ b/lib/timeutils.c @@ -0,0 +1,707 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * First set of functions in this file are part of systemd, and were + * copied to util-linux at August 2013. + * + * Copyright 2010 Lennart Poettering + * Copyright (C) 2014 Karel Zak <kzak@redhat.com> + * + * This is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + */ +#include <assert.h> +#include <ctype.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <sys/time.h> +#include <inttypes.h> + +#include "c.h" +#include "nls.h" +#include "strutils.h" +#include "timeutils.h" + +#define WHITESPACE " \t\n\r" + +#define streq(a,b) (strcmp((a),(b)) == 0) + +static int parse_sec(const char *t, usec_t *usec) +{ + static const struct { + const char *suffix; + usec_t usec; + } table[] = { + { "seconds", USEC_PER_SEC }, + { "second", USEC_PER_SEC }, + { "sec", USEC_PER_SEC }, + { "s", USEC_PER_SEC }, + { "minutes", USEC_PER_MINUTE }, + { "minute", USEC_PER_MINUTE }, + { "min", USEC_PER_MINUTE }, + { "months", USEC_PER_MONTH }, + { "month", USEC_PER_MONTH }, + { "msec", USEC_PER_MSEC }, + { "ms", USEC_PER_MSEC }, + { "m", USEC_PER_MINUTE }, + { "hours", USEC_PER_HOUR }, + { "hour", USEC_PER_HOUR }, + { "hr", USEC_PER_HOUR }, + { "h", USEC_PER_HOUR }, + { "days", USEC_PER_DAY }, + { "day", USEC_PER_DAY }, + { "d", USEC_PER_DAY }, + { "weeks", USEC_PER_WEEK }, + { "week", USEC_PER_WEEK }, + { "w", USEC_PER_WEEK }, + { "years", USEC_PER_YEAR }, + { "year", USEC_PER_YEAR }, + { "y", USEC_PER_YEAR }, + { "usec", 1ULL }, + { "us", 1ULL }, + { "", USEC_PER_SEC }, /* default is sec */ + }; + + const char *p; + usec_t r = 0; + int something = FALSE; + + assert(t); + assert(usec); + + p = t; + for (;;) { + long long l, z = 0; + char *e; + unsigned i, n = 0; + + p += strspn(p, WHITESPACE); + + if (*p == 0) { + if (!something) + return -EINVAL; + + break; + } + + errno = 0; + l = strtoll(p, &e, 10); + + if (errno > 0) + return -errno; + + if (l < 0) + return -ERANGE; + + if (*e == '.') { + char *b = e + 1; + + errno = 0; + z = strtoll(b, &e, 10); + if (errno > 0) + return -errno; + + if (z < 0) + return -ERANGE; + + if (e == b) + return -EINVAL; + + n = e - b; + + } else if (e == p) + return -EINVAL; + + e += strspn(e, WHITESPACE); + + for (i = 0; i < ARRAY_SIZE(table); i++) + if (startswith(e, table[i].suffix)) { + usec_t k = (usec_t) z * table[i].usec; + + for (; n > 0; n--) + k /= 10; + + r += (usec_t) l *table[i].usec + k; + p = e + strlen(table[i].suffix); + + something = TRUE; + break; + } + + if (i >= ARRAY_SIZE(table)) + return -EINVAL; + + } + + *usec = r; + + return 0; +} + +static int parse_subseconds(const char *t, usec_t *usec) +{ + usec_t ret = 0; + int factor = USEC_PER_SEC / 10; + + if (*t != '.' && *t != ',') + return -1; + + while (*(++t)) { + if (!isdigit(*t) || factor < 1) + return -1; + + ret += ((usec_t) *t - '0') * factor; + factor /= 10; + } + + *usec = ret; + return 0; +} + +static int parse_timestamp_reference(time_t x, const char *t, usec_t *usec) +{ + static const struct { + const char *name; + const int nr; + } day_nr[] = { + { "Sunday", 0 }, + { "Sun", 0 }, + { "Monday", 1 }, + { "Mon", 1 }, + { "Tuesday", 2 }, + { "Tue", 2 }, + { "Wednesday", 3 }, + { "Wed", 3 }, + { "Thursday", 4 }, + { "Thu", 4 }, + { "Friday", 5 }, + { "Fri", 5 }, + { "Saturday", 6 }, + { "Sat", 6 }, + }; + + const char *k; + struct tm tm, copy; + usec_t plus = 0, minus = 0, ret = 0; + int r, weekday = -1; + unsigned i; + + /* + * Allowed syntaxes: + * + * 2012-09-22 16:34:22 ! + * 2012-09-22T16:34:22 ! + * 20120922163422 ! + * @1348331662 ! (seconds since the Epoch (1970-01-01 00:00 UTC)) + * 2012-09-22 16:34 (seconds will be set to 0) + * 2012-09-22 (time will be set to 00:00:00) + * 16:34:22 ! (date will be set to today) + * 16:34 (date will be set to today, seconds to 0) + * now + * yesterday (time is set to 00:00:00) + * today (time is set to 00:00:00) + * tomorrow (time is set to 00:00:00) + * +5min + * -5days + * + * Syntaxes marked with '!' also optionally allow up to six digits of + * subsecond granularity, separated by '.' or ',': + * + * 2012-09-22 16:34:22.12 + * 2012-09-22 16:34:22.123456 + * + * + */ + + assert(t); + assert(usec); + + localtime_r(&x, &tm); + tm.tm_isdst = -1; + + if (streq(t, "now")) + goto finish; + + else if (streq(t, "today")) { + tm.tm_sec = tm.tm_min = tm.tm_hour = 0; + goto finish; + + } else if (streq(t, "yesterday")) { + tm.tm_mday--; + tm.tm_sec = tm.tm_min = tm.tm_hour = 0; + goto finish; + + } else if (streq(t, "tomorrow")) { + tm.tm_mday++; + tm.tm_sec = tm.tm_min = tm.tm_hour = 0; + goto finish; + + } else if (t[0] == '+') { + + r = parse_sec(t + 1, &plus); + if (r < 0) + return r; + + goto finish; + } else if (t[0] == '-') { + + r = parse_sec(t + 1, &minus); + if (r < 0) + return r; + + goto finish; + } else if (t[0] == '@') { + k = strptime(t + 1, "%s", &tm); + if (k && *k == 0) + goto finish; + else if (k && parse_subseconds(k, &ret) == 0) + goto finish; + + return -EINVAL; + } else if (endswith(t, " ago")) { + char *z; + + z = strndup(t, strlen(t) - 4); + if (!z) + return -ENOMEM; + + r = parse_sec(z, &minus); + free(z); + if (r < 0) + return r; + + goto finish; + } + + for (i = 0; i < ARRAY_SIZE(day_nr); i++) { + size_t skip; + + if (!startswith_no_case(t, day_nr[i].name)) + continue; + + skip = strlen(day_nr[i].name); + if (t[skip] != ' ') + continue; + + weekday = day_nr[i].nr; + t += skip + 1; + break; + } + + copy = tm; + k = strptime(t, "%y-%m-%d %H:%M:%S", &tm); + if (k && *k == 0) + goto finish; + else if (k && parse_subseconds(k, &ret) == 0) + goto finish; + + tm = copy; + k = strptime(t, "%Y-%m-%d %H:%M:%S", &tm); + if (k && *k == 0) + goto finish; + else if (k && parse_subseconds(k, &ret) == 0) + goto finish; + + tm = copy; + k = strptime(t, "%Y-%m-%dT%H:%M:%S", &tm); + if (k && *k == 0) + goto finish; + else if (k && parse_subseconds(k, &ret) == 0) + goto finish; + + tm = copy; + k = strptime(t, "%y-%m-%d %H:%M", &tm); + if (k && *k == 0) { + tm.tm_sec = 0; + goto finish; + } + + tm = copy; + k = strptime(t, "%Y-%m-%d %H:%M", &tm); + if (k && *k == 0) { + tm.tm_sec = 0; + goto finish; + } + + tm = copy; + k = strptime(t, "%y-%m-%d", &tm); + if (k && *k == 0) { + tm.tm_sec = tm.tm_min = tm.tm_hour = 0; + goto finish; + } + + tm = copy; + k = strptime(t, "%Y-%m-%d", &tm); + if (k && *k == 0) { + tm.tm_sec = tm.tm_min = tm.tm_hour = 0; + goto finish; + } + + tm = copy; + k = strptime(t, "%H:%M:%S", &tm); + if (k && *k == 0) + goto finish; + else if (k && parse_subseconds(k, &ret) == 0) + goto finish; + + tm = copy; + k = strptime(t, "%H:%M", &tm); + if (k && *k == 0) { + tm.tm_sec = 0; + goto finish; + } + + tm = copy; + k = strptime(t, "%Y%m%d%H%M%S", &tm); + if (k && *k == 0) + goto finish; + else if (k && parse_subseconds(k, &ret) == 0) + goto finish; + + return -EINVAL; + + finish: + x = mktime(&tm); + if (x == (time_t)-1) + return -EINVAL; + + if (weekday >= 0 && tm.tm_wday != weekday) + return -EINVAL; + + ret += (usec_t) x * USEC_PER_SEC; + + ret += plus; + if (ret > minus) + ret -= minus; + else + ret = 0; + + *usec = ret; + + return 0; +} + +int parse_timestamp(const char *t, usec_t *usec) +{ + return parse_timestamp_reference(time(NULL), t, usec); +} + +/* Returns the difference in seconds between its argument and GMT. If if TP is + * invalid or no DST information is available default to UTC, that is, zero. + * tzset is called so, for example, 'TZ="UTC" hwclock' will work as expected. + * Derived from glibc/time/strftime_l.c + */ +int get_gmtoff(const struct tm *tp) +{ + if (tp->tm_isdst < 0) + return 0; + +#if HAVE_TM_GMTOFF + return tp->tm_gmtoff; +#else + struct tm tm; + struct tm gtm; + struct tm ltm = *tp; + time_t lt; + + tzset(); + lt = mktime(<m); + /* Check if mktime returning -1 is an error or a valid time_t */ + if (lt == (time_t) -1) { + if (! localtime_r(<, &tm) + || ((ltm.tm_sec ^ tm.tm_sec) + | (ltm.tm_min ^ tm.tm_min) + | (ltm.tm_hour ^ tm.tm_hour) + | (ltm.tm_mday ^ tm.tm_mday) + | (ltm.tm_mon ^ tm.tm_mon) + | (ltm.tm_year ^ tm.tm_year))) + return 0; + } + + if (! gmtime_r(<, >m)) + return 0; + + /* Calculate the GMT offset, that is, the difference between the + * TP argument (ltm) and GMT (gtm). + * + * Compute intervening leap days correctly even if year is negative. + * Take care to avoid int overflow in leap day calculations, but it's OK + * to assume that A and B are close to each other. + */ + int a4 = (ltm.tm_year >> 2) + (1900 >> 2) - ! (ltm.tm_year & 3); + int b4 = (gtm.tm_year >> 2) + (1900 >> 2) - ! (gtm.tm_year & 3); + int a100 = a4 / 25 - (a4 % 25 < 0); + int b100 = b4 / 25 - (b4 % 25 < 0); + int a400 = a100 >> 2; + int b400 = b100 >> 2; + int intervening_leap_days = (a4 - b4) - (a100 - b100) + (a400 - b400); + + int years = ltm.tm_year - gtm.tm_year; + int days = (365 * years + intervening_leap_days + + (ltm.tm_yday - gtm.tm_yday)); + + return (60 * (60 * (24 * days + (ltm.tm_hour - gtm.tm_hour)) + + (ltm.tm_min - gtm.tm_min)) + (ltm.tm_sec - gtm.tm_sec)); +#endif +} + +static int format_iso_time(struct tm *tm, suseconds_t usec, int flags, char *buf, size_t bufsz) +{ + char *p = buf; + int len; + + if (flags & ISO_DATE) { + len = snprintf(p, bufsz, "%4ld-%.2d-%.2d", + tm->tm_year + (long) 1900, + tm->tm_mon + 1, tm->tm_mday); + if (len < 0 || (size_t) len > bufsz) + goto err; + bufsz -= len; + p += len; + } + + if ((flags & ISO_DATE) && (flags & ISO_TIME)) { + if (bufsz < 1) + goto err; + *p++ = (flags & ISO_T) ? 'T' : ' '; + bufsz--; + } + + if (flags & ISO_TIME) { + len = snprintf(p, bufsz, "%02d:%02d:%02d", tm->tm_hour, + tm->tm_min, tm->tm_sec); + if (len < 0 || (size_t) len > bufsz) + goto err; + bufsz -= len; + p += len; + } + + if (flags & ISO_DOTUSEC) { + len = snprintf(p, bufsz, ".%06"PRId64, (int64_t) usec); + if (len < 0 || (size_t) len > bufsz) + goto err; + bufsz -= len; + p += len; + + } else if (flags & ISO_COMMAUSEC) { + len = snprintf(p, bufsz, ",%06"PRId64, (int64_t) usec); + if (len < 0 || (size_t) len > bufsz) + goto err; + bufsz -= len; + p += len; + } + + if (flags & ISO_TIMEZONE) { + int tmin = get_gmtoff(tm) / 60; + int zhour = tmin / 60; + int zmin = abs(tmin % 60); + len = snprintf(p, bufsz, "%+03d:%02d", zhour,zmin); + if (len < 0 || (size_t) len > bufsz) + goto err; + } + return 0; + err: + warnx(_("format_iso_time: buffer overflow.")); + return -1; +} + +/* timeval to ISO 8601 */ +int strtimeval_iso(struct timeval *tv, int flags, char *buf, size_t bufsz) +{ + struct tm tm; + struct tm *rc; + + if (flags & ISO_GMTIME) + rc = gmtime_r(&tv->tv_sec, &tm); + else + rc = localtime_r(&tv->tv_sec, &tm); + + if (rc) + return format_iso_time(&tm, tv->tv_usec, flags, buf, bufsz); + + warnx(_("time %"PRId64" is out of range."), (int64_t)(tv->tv_sec)); + return -1; +} + +/* struct tm to ISO 8601 */ +int strtm_iso(struct tm *tm, int flags, char *buf, size_t bufsz) +{ + return format_iso_time(tm, 0, flags, buf, bufsz); +} + +/* time_t to ISO 8601 */ +int strtime_iso(const time_t *t, int flags, char *buf, size_t bufsz) +{ + struct tm tm; + struct tm *rc; + + if (flags & ISO_GMTIME) + rc = gmtime_r(t, &tm); + else + rc = localtime_r(t, &tm); + + if (rc) + return format_iso_time(&tm, 0, flags, buf, bufsz); + + warnx(_("time %"PRId64" is out of range."), (int64_t)*t); + return -1; +} + +/* relative time functions */ +static inline int time_is_thisyear(struct tm const *const tm, + struct tm const *const tmnow) +{ + return tm->tm_year == tmnow->tm_year; +} + +static inline int time_is_today(struct tm const *const tm, + struct tm const *const tmnow) +{ + return (tm->tm_yday == tmnow->tm_yday && + time_is_thisyear(tm, tmnow)); +} + +int strtime_short(const time_t *t, struct timeval *now, int flags, char *buf, size_t bufsz) +{ + struct tm tm, tmnow; + int rc = 0; + + if (now->tv_sec == 0) + gettimeofday(now, NULL); + + localtime_r(t, &tm); + localtime_r(&now->tv_sec, &tmnow); + + if (time_is_today(&tm, &tmnow)) { + rc = snprintf(buf, bufsz, "%02d:%02d", tm.tm_hour, tm.tm_min); + if (rc < 0 || (size_t) rc > bufsz) + return -1; + rc = 1; + + } else if (time_is_thisyear(&tm, &tmnow)) { + if (flags & UL_SHORTTIME_THISYEAR_HHMM) + rc = strftime(buf, bufsz, "%b%d/%H:%M", &tm); + else + rc = strftime(buf, bufsz, "%b%d", &tm); + } else + rc = strftime(buf, bufsz, "%Y-%b%d", &tm); + + return rc <= 0 ? -1 : 0; +} + +#ifndef HAVE_TIMEGM +time_t timegm(struct tm *tm) +{ + const char *zone = getenv("TZ"); + time_t ret; + + setenv("TZ", "", 1); + tzset(); + ret = mktime(tm); + if (zone) + setenv("TZ", zone, 1); + else + unsetenv("TZ"); + tzset(); + return ret; +} +#endif /* HAVE_TIMEGM */ + +#ifdef TEST_PROGRAM_TIMEUTILS + +static int run_unittest_timestamp(void) +{ + int rc = EXIT_SUCCESS; + time_t reference = 1674180427; + static const struct testcase { + const char * const input; + usec_t expected; + } testcases[] = { + { "2012-09-22 16:34:22" , 1348331662000000 }, + { "2012-09-22 16:34:22,012", 1348331662012000 }, + { "2012-09-22 16:34:22.012", 1348331662012000 }, + { "@1348331662" , 1348331662000000 }, + { "@1348331662.234567" , 1348331662234567 }, + { "2012-09-22 16:34" , 1348331640000000 }, + { "2012-09-22" , 1348272000000000 }, + { "16:34:22" , 1674232462000000 }, + { "16:34:22,123456" , 1674232462123456 }, + { "16:34:22.123456" , 1674232462123456 }, + { "16:34" , 1674232440000000 }, + { "now" , 1674180427000000 }, + { "yesterday" , 1674086400000000 }, + { "today" , 1674172800000000 }, + { "tomorrow" , 1674259200000000 }, + { "+5min" , 1674180727000000 }, + { "-5days" , 1673748427000000 }, + { "20120922163422" , 1348331662000000 }, + }; + + setenv("TZ", "GMT", 1); + tzset(); + + for (size_t i = 0; i < ARRAY_SIZE(testcases); i++) { + struct testcase t = testcases[i]; + usec_t result; + int r = parse_timestamp_reference(reference, t.input, &result); + if (r) { + fprintf(stderr, "Could not parse '%s'\n", t.input); + rc = EXIT_FAILURE; + } + + if (result != t.expected) { + fprintf(stderr, "#%02zu %-25s: %"PRId64" != %"PRId64"\n", + i, t.input, result, t.expected); + rc = EXIT_FAILURE; + } + } + + return rc; +} + +int main(int argc, char *argv[]) +{ + struct timeval tv = { 0 }; + char buf[ISO_BUFSIZ]; + + if (argc < 2) { + fprintf(stderr, "usage: %s [<time> [<usec>]] | [--timestamp <str>] | [--unittest-timestamp]\n", argv[0]); + exit(EXIT_FAILURE); + } + + if (strcmp(argv[1], "--unittest-timestamp") == 0) { + return run_unittest_timestamp(); + } + + if (strcmp(argv[1], "--timestamp") == 0) { + usec_t usec = 0; + + parse_timestamp(argv[2], &usec); + tv.tv_sec = (time_t) (usec / 1000000); + tv.tv_usec = usec % 1000000; + } else { + tv.tv_sec = strtos64_or_err(argv[1], "failed to parse <time>"); + if (argc == 3) + tv.tv_usec = strtos64_or_err(argv[2], "failed to parse <usec>"); + } + + strtimeval_iso(&tv, ISO_DATE, buf, sizeof(buf)); + printf("Date: '%s'\n", buf); + + strtimeval_iso(&tv, ISO_TIME, buf, sizeof(buf)); + printf("Time: '%s'\n", buf); + + strtimeval_iso(&tv, ISO_DATE | ISO_TIME | ISO_COMMAUSEC | ISO_T, + buf, sizeof(buf)); + printf("Full: '%s'\n", buf); + + strtimeval_iso(&tv, ISO_TIMESTAMP_DOT, buf, sizeof(buf)); + printf("Zone: '%s'\n", buf); + + return EXIT_SUCCESS; +} + +#endif /* TEST_PROGRAM_TIMEUTILS */ |