From 6b04af6072dd052f7d71c826679839b3dba58e53 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sun, 12 Feb 2023 05:30:34 +0900 Subject: [PATCH 01/15] time-util: drop unused definition of FORMAT_TIMESTAMP_WIDTH --- src/basic/time-util.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/basic/time-util.h b/src/basic/time-util.h index 0ed19d04adb..554a9618d99 100644 --- a/src/basic/time-util.h +++ b/src/basic/time-util.h @@ -66,7 +66,6 @@ typedef enum TimestampStyle { /* We assume a maximum timezone length of 6. TZNAME_MAX is not defined on Linux, but glibc internally initializes this * to 6. Let's rely on that. */ #define FORMAT_TIMESTAMP_MAX (3U+1U+10U+1U+8U+1U+6U+1U+6U+1U) -#define FORMAT_TIMESTAMP_WIDTH 28U /* when outputting, assume this width */ #define FORMAT_TIMESTAMP_RELATIVE_MAX 256U #define FORMAT_TIMESPAN_MAX 64U From 7464953f33dbb70f668c7854b5750db5c3f7dc66 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 01:38:57 +0900 Subject: [PATCH 02/15] time-util: drop redundant space --- src/basic/time-util.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index bae62ce411f..b26c94e4924 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -228,7 +228,7 @@ nsec_t timespec_load_nsec(const struct timespec *ts) { return (nsec_t) ts->tv_sec * NSEC_PER_SEC + (nsec_t) ts->tv_nsec; } -struct timespec *timespec_store(struct timespec *ts, usec_t u) { +struct timespec *timespec_store(struct timespec *ts, usec_t u) { assert(ts); if (u == USEC_INFINITY || @@ -244,7 +244,7 @@ struct timespec *timespec_store(struct timespec *ts, usec_t u) { return ts; } -struct timespec *timespec_store_nsec(struct timespec *ts, nsec_t n) { +struct timespec *timespec_store_nsec(struct timespec *ts, nsec_t n) { assert(ts); if (n == NSEC_INFINITY || From d227a42aadf04c23c668ac3089bc7b4a9baaf7e1 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 01:40:56 +0900 Subject: [PATCH 03/15] time-util: use DEFINE_STRING_TABLE_LOOKUP_TO_STRING() macro --- src/basic/time-util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index b26c94e4924..f9d74f771ce 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -1617,7 +1617,7 @@ static const char* const timestamp_style_table[_TIMESTAMP_STYLE_MAX] = { }; /* Use the macro for enum → string to allow for aliases */ -_DEFINE_STRING_TABLE_LOOKUP_TO_STRING(timestamp_style, TimestampStyle,); +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(timestamp_style, TimestampStyle); /* For the string → enum mapping we use the generic implementation, but also support two aliases */ TimestampStyle timestamp_style_from_string(const char *s) { From e01a8fdd2645c06cdb9057bd5b8a45ab02c0d6ee Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 01:41:38 +0900 Subject: [PATCH 04/15] time-util: align string table --- src/basic/time-util.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index f9d74f771ce..2498ea5159f 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -1610,10 +1610,10 @@ int time_change_fd(void) { static const char* const timestamp_style_table[_TIMESTAMP_STYLE_MAX] = { [TIMESTAMP_PRETTY] = "pretty", - [TIMESTAMP_US] = "us", - [TIMESTAMP_UTC] = "utc", + [TIMESTAMP_US] = "us", + [TIMESTAMP_UTC] = "utc", [TIMESTAMP_US_UTC] = "us+utc", - [TIMESTAMP_UNIX] = "unix", + [TIMESTAMP_UNIX] = "unix", }; /* Use the macro for enum → string to allow for aliases */ From cf98b66d1ad0ff0e9ee0444861069ebade038dbb Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 02:04:31 +0900 Subject: [PATCH 05/15] time-util: rename variables --- src/basic/time-util.c | 86 +++++++++++++++++++++---------------------- src/basic/time-util.h | 16 ++++---- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 2498ea5159f..7f5605953f8 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -605,7 +605,7 @@ char* format_timespan(char *buf, size_t l, usec_t t, usec_t accuracy) { return buf; } -static int parse_timestamp_impl(const char *t, usec_t *usec, bool with_tz) { +static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { static const struct { const char *name; const int nr; @@ -629,7 +629,7 @@ static int parse_timestamp_impl(const char *t, usec_t *usec, bool with_tz) { const char *k, *utc = NULL, *tzn = NULL; struct tm tm, copy; time_t x; - usec_t x_usec, plus = 0, minus = 0, ret; + usec_t usec, x_usec, plus = 0, minus = 0; int r, weekday = -1, dst = -1; size_t i; @@ -652,9 +652,9 @@ static int parse_timestamp_impl(const char *t, usec_t *usec, bool with_tz) { assert(t); if (t[0] == '@' && !with_tz) - return parse_sec(t + 1, usec); + return parse_sec(t + 1, ret); - ret = now(CLOCK_REALTIME); + usec = now(CLOCK_REALTIME); if (!with_tz) { if (streq(t, "now")) @@ -735,7 +735,7 @@ static int parse_timestamp_impl(const char *t, usec_t *usec, bool with_tz) { } } - x = (time_t) (ret / USEC_PER_SEC); + x = (time_t) (usec / USEC_PER_SEC); x_usec = 0; if (!localtime_or_gmtime_r(&x, &tm, utc)) @@ -872,24 +872,24 @@ from_tm: if (x < 0) return -EINVAL; - ret = (usec_t) x * USEC_PER_SEC + x_usec; - if (ret > USEC_TIMESTAMP_FORMATTABLE_MAX) + usec = (usec_t) x * USEC_PER_SEC + x_usec; + if (usec > USEC_TIMESTAMP_FORMATTABLE_MAX) return -EINVAL; finish: - if (ret + plus < ret) /* overflow? */ + if (usec + plus < usec) /* overflow? */ return -EINVAL; - ret += plus; - if (ret > USEC_TIMESTAMP_FORMATTABLE_MAX) + usec += plus; + if (usec > USEC_TIMESTAMP_FORMATTABLE_MAX) return -EINVAL; - if (ret >= minus) - ret -= minus; + if (usec >= minus) + usec -= minus; else return -EINVAL; - if (usec) - *usec = ret; + if (ret) + *ret = usec; return 0; } @@ -898,7 +898,7 @@ typedef struct ParseTimestampResult { int return_value; } ParseTimestampResult; -int parse_timestamp(const char *t, usec_t *usec) { +int parse_timestamp(const char *t, usec_t *ret) { char *last_space, *tz = NULL; ParseTimestampResult *shared, tmp; int r; @@ -908,7 +908,7 @@ int parse_timestamp(const char *t, usec_t *usec) { tz = last_space + 1; if (!tz || endswith_no_case(t, " UTC")) - return parse_timestamp_impl(t, usec, false); + return parse_timestamp_impl(t, ret, false); shared = mmap(NULL, sizeof *shared, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); if (shared == MAP_FAILED) @@ -950,13 +950,13 @@ int parse_timestamp(const char *t, usec_t *usec) { if (munmap(shared, sizeof *shared) != 0) return negative_errno(); - if (tmp.return_value == 0 && usec) - *usec = tmp.usec; + if (tmp.return_value == 0 && ret) + *ret = tmp.usec; return tmp.return_value; } -static const char* extract_multiplier(const char *p, usec_t *multiplier) { +static const char* extract_multiplier(const char *p, usec_t *ret) { static const struct { const char *suffix; usec_t usec; @@ -997,7 +997,7 @@ static const char* extract_multiplier(const char *p, usec_t *multiplier) { e = startswith(p, table[i].suffix); if (e) { - *multiplier = table[i].usec; + *ret = table[i].usec; return e; } } @@ -1005,9 +1005,9 @@ static const char* extract_multiplier(const char *p, usec_t *multiplier) { return p; } -int parse_time(const char *t, usec_t *usec, usec_t default_unit) { +int parse_time(const char *t, usec_t *ret, usec_t default_unit) { const char *p, *s; - usec_t r = 0; + usec_t usec = 0; bool something = false; assert(t); @@ -1022,8 +1022,8 @@ int parse_time(const char *t, usec_t *usec, usec_t default_unit) { if (*s != 0) return -EINVAL; - if (usec) - *usec = USEC_INFINITY; + if (ret) + *ret = USEC_INFINITY; return 0; } @@ -1070,10 +1070,10 @@ int parse_time(const char *t, usec_t *usec, usec_t default_unit) { return -ERANGE; k = (usec_t) l * multiplier; - if (k >= USEC_INFINITY - r) + if (k >= USEC_INFINITY - usec) return -ERANGE; - r += k; + usec += k; something = true; @@ -1083,10 +1083,10 @@ int parse_time(const char *t, usec_t *usec, usec_t default_unit) { for (b = e + 1; *b >= '0' && *b <= '9'; b++, m /= 10) { k = (usec_t) (*b - '0') * m; - if (k >= USEC_INFINITY - r) + if (k >= USEC_INFINITY - usec) return -ERANGE; - r += k; + usec += k; } /* Don't allow "0.-0", "3.+1", "3. 1", "3.sec" or "3.hoge" */ @@ -1095,13 +1095,13 @@ int parse_time(const char *t, usec_t *usec, usec_t default_unit) { } } - if (usec) - *usec = r; + if (ret) + *ret = usec; return 0; } -int parse_sec(const char *t, usec_t *usec) { - return parse_time(t, usec, USEC_PER_SEC); +int parse_sec(const char *t, usec_t *ret) { + return parse_time(t, ret, USEC_PER_SEC); } int parse_sec_fix_0(const char *t, usec_t *ret) { @@ -1128,7 +1128,7 @@ int parse_sec_def_infinity(const char *t, usec_t *ret) { return parse_sec(t, ret); } -static const char* extract_nsec_multiplier(const char *p, nsec_t *multiplier) { +static const char* extract_nsec_multiplier(const char *p, nsec_t *ret) { static const struct { const char *suffix; nsec_t nsec; @@ -1173,7 +1173,7 @@ static const char* extract_nsec_multiplier(const char *p, nsec_t *multiplier) { e = startswith(p, table[i].suffix); if (e) { - *multiplier = table[i].nsec; + *ret = table[i].nsec; return e; } } @@ -1181,13 +1181,13 @@ static const char* extract_nsec_multiplier(const char *p, nsec_t *multiplier) { return p; } -int parse_nsec(const char *t, nsec_t *nsec) { +int parse_nsec(const char *t, nsec_t *ret) { const char *p, *s; - nsec_t r = 0; + nsec_t nsec = 0; bool something = false; assert(t); - assert(nsec); + assert(ret); p = t; @@ -1198,7 +1198,7 @@ int parse_nsec(const char *t, nsec_t *nsec) { if (*s != 0) return -EINVAL; - *nsec = NSEC_INFINITY; + *ret = NSEC_INFINITY; return 0; } @@ -1245,10 +1245,10 @@ int parse_nsec(const char *t, nsec_t *nsec) { return -ERANGE; k = (nsec_t) l * multiplier; - if (k >= NSEC_INFINITY - r) + if (k >= NSEC_INFINITY - nsec) return -ERANGE; - r += k; + nsec += k; something = true; @@ -1258,10 +1258,10 @@ int parse_nsec(const char *t, nsec_t *nsec) { for (b = e + 1; *b >= '0' && *b <= '9'; b++, m /= 10) { k = (nsec_t) (*b - '0') * m; - if (k >= NSEC_INFINITY - r) + if (k >= NSEC_INFINITY - nsec) return -ERANGE; - r += k; + nsec += k; } /* Don't allow "0.-0", "3.+1", "3. 1", "3.sec" or "3.hoge" */ @@ -1270,7 +1270,7 @@ int parse_nsec(const char *t, nsec_t *nsec) { } } - *nsec = r; + *ret = nsec; return 0; } diff --git a/src/basic/time-util.h b/src/basic/time-util.h index 554a9618d99..087f5324ef0 100644 --- a/src/basic/time-util.h +++ b/src/basic/time-util.h @@ -146,15 +146,15 @@ static inline char* format_timestamp(char *buf, size_t l, usec_t t) { #define FORMAT_TIMESTAMP_STYLE(t, style) \ format_timestamp_style((char[FORMAT_TIMESTAMP_MAX]){}, FORMAT_TIMESTAMP_MAX, t, style) -int parse_timestamp(const char *t, usec_t *usec); +int parse_timestamp(const char *t, usec_t *ret); -int parse_sec(const char *t, usec_t *usec); -int parse_sec_fix_0(const char *t, usec_t *usec); -int parse_sec_def_infinity(const char *t, usec_t *usec); -int parse_time(const char *t, usec_t *usec, usec_t default_unit); -int parse_nsec(const char *t, nsec_t *nsec); +int parse_sec(const char *t, usec_t *ret); +int parse_sec_fix_0(const char *t, usec_t *ret); +int parse_sec_def_infinity(const char *t, usec_t *ret); +int parse_time(const char *t, usec_t *ret, usec_t default_unit); +int parse_nsec(const char *t, nsec_t *ret); -int get_timezones(char ***l); +int get_timezones(char ***ret); int verify_timezone(const char *name, int log_level); static inline bool timezone_is_valid(const char *name, int log_level) { return verify_timezone(name, log_level) >= 0; @@ -164,7 +164,7 @@ bool clock_supported(clockid_t clock); usec_t usec_shift_clock(usec_t, clockid_t from, clockid_t to); -int get_timezone(char **timezone); +int get_timezone(char **ret); time_t mktime_or_timegm(struct tm *tm, bool utc); struct tm *localtime_or_gmtime_r(const time_t *t, struct tm *tm, bool utc); From dff3bddc5416834d42cc682cb544732a4b91db3b Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 02:06:13 +0900 Subject: [PATCH 06/15] time-util: add assertions --- src/basic/time-util.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 7f5605953f8..f563b8560cf 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -171,6 +171,8 @@ dual_timestamp* dual_timestamp_from_monotonic(dual_timestamp *ts, usec_t u) { dual_timestamp* dual_timestamp_from_boottime(dual_timestamp *ts, usec_t u) { usec_t nowm; + assert(ts); + if (u == USEC_INFINITY) { ts->realtime = ts->monotonic = USEC_INFINITY; return ts; @@ -183,6 +185,7 @@ dual_timestamp* dual_timestamp_from_boottime(dual_timestamp *ts, usec_t u) { } usec_t triple_timestamp_by_clock(triple_timestamp *ts, clockid_t clock) { + assert(ts); switch (clock) { @@ -422,6 +425,8 @@ char* format_timestamp_relative_full(char *buf, size_t l, usec_t t, bool implici const char *s; usec_t n, d; + assert(buf); + if (!timestamp_is_set(t)) return NULL; @@ -903,6 +908,8 @@ int parse_timestamp(const char *t, usec_t *ret) { ParseTimestampResult *shared, tmp; int r; + assert(t); + last_space = strrchr(t, ' '); if (last_space != NULL && timezone_is_valid(last_space + 1, LOG_DEBUG)) tz = last_space + 1; @@ -992,6 +999,9 @@ static const char* extract_multiplier(const char *p, usec_t *ret) { { "µs", 1ULL }, }; + assert(p); + assert(ret); + for (size_t i = 0; i < ELEMENTSOF(table); i++) { char *e; @@ -1120,6 +1130,9 @@ int parse_sec_fix_0(const char *t, usec_t *ret) { } int parse_sec_def_infinity(const char *t, usec_t *ret) { + assert(t); + assert(ret); + t += strspn(t, WHITESPACE); if (isempty(t)) { *ret = USEC_INFINITY; @@ -1168,6 +1181,9 @@ static const char* extract_nsec_multiplier(const char *p, nsec_t *ret) { }; size_t i; + assert(p); + assert(ret); + for (i = 0; i < ELEMENTSOF(table); i++) { char *e; @@ -1321,6 +1337,8 @@ static int get_timezones_from_tzdata_zi(char ***ret) { _cleanup_strv_free_ char **zones = NULL; int r; + assert(ret); + f = fopen("/usr/share/zoneinfo/tzdata.zi", "re"); if (!f) return -errno; @@ -1478,6 +1496,8 @@ int get_timezone(char **ret) { char *z; int r; + assert(ret); + r = readlink_malloc("/etc/localtime", &t); if (r == -ENOENT) { /* If the symlink does not exist, assume "UTC", like glibc does */ @@ -1507,10 +1527,15 @@ int get_timezone(char **ret) { } time_t mktime_or_timegm(struct tm *tm, bool utc) { + assert(tm); + return utc ? timegm(tm) : mktime(tm); } struct tm *localtime_or_gmtime_r(const time_t *t, struct tm *tm, bool utc) { + assert(t); + assert(tm); + return utc ? gmtime_r(t, tm) : localtime_r(t, tm); } From 17d1ebfc43c3b971d20ff2806acc634ee153eef6 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 03:43:58 +0900 Subject: [PATCH 07/15] time-util: drop redundant else --- src/basic/time-util.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index f563b8560cf..54624afda14 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -665,21 +665,23 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { if (streq(t, "now")) goto finish; - else if (t[0] == '+') { + if (t[0] == '+') { r = parse_sec(t+1, &plus); if (r < 0) return r; goto finish; + } - } else if (t[0] == '-') { + if (t[0] == '-') { r = parse_sec(t+1, &minus); if (r < 0) return r; goto finish; + } - } else if ((k = endswith(t, " ago"))) { + if ((k = endswith(t, " ago"))) { t = strndupa_safe(t, k - t); r = parse_sec(t, &minus); @@ -687,8 +689,9 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { return r; goto finish; + } - } else if ((k = endswith(t, " left"))) { + if ((k = endswith(t, " left"))) { t = strndupa_safe(t, k - t); r = parse_sec(t, &plus); From 804537bdc420bb82e54b455b7a10d542c8f029dd Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 03:41:26 +0900 Subject: [PATCH 08/15] time-util: do not use strdupa() The input string may come from command line, config files. --- src/basic/time-util.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 54624afda14..15f908b137d 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -682,9 +682,13 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { } if ((k = endswith(t, " ago"))) { - t = strndupa_safe(t, k - t); + _cleanup_free_ char *buf = NULL; - r = parse_sec(t, &minus); + buf = strndup(t, k - t); + if (!buf) + return -ENOMEM; + + r = parse_sec(buf, &minus); if (r < 0) return r; @@ -692,9 +696,13 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { } if ((k = endswith(t, " left"))) { - t = strndupa_safe(t, k - t); + _cleanup_free_ char *buf = NULL; - r = parse_sec(t, &plus); + buf = strndup(t, k - t); + if (!buf) + return -ENOMEM; + + r = parse_sec(buf, &plus); if (r < 0) return r; From f2ecfd8bc1e6d09173e9f98c5ac1b19b755a3c25 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 04:14:24 +0900 Subject: [PATCH 09/15] time-util: use result from startswith_no_case() No functional change, just refactoring. --- src/basic/time-util.c | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 15f908b137d..73def365a48 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -636,7 +636,6 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { time_t x; usec_t usec, x_usec, plus = 0, minus = 0; int r, weekday = -1, dst = -1; - size_t i; /* Allowed syntaxes: * @@ -776,18 +775,13 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { goto from_tm; } - for (i = 0; i < ELEMENTSOF(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] != ' ') + for (size_t i = 0; i < ELEMENTSOF(day_nr); i++) { + k = startswith_no_case(t, day_nr[i].name); + if (!k || *k != ' ') continue; weekday = day_nr[i].nr; - t += skip + 1; + t = k + 1; break; } From db43717e982e1361eee4bdcd92167d6c47eb627c Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 04:27:27 +0900 Subject: [PATCH 10/15] time-util: use usec_add() and usec_sub_unsigned() And move the check with USEC_TIMESTAMP_FORMATTABLE_MAX at the end, as usec_add() can handle overflow correctly. --- src/basic/time-util.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 73def365a48..5ffd6bc5749 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -882,20 +882,17 @@ from_tm: if (x < 0) return -EINVAL; - usec = (usec_t) x * USEC_PER_SEC + x_usec; - if (usec > USEC_TIMESTAMP_FORMATTABLE_MAX) - return -EINVAL; + usec = usec_add(x * USEC_PER_SEC, x_usec); finish: - if (usec + plus < usec) /* overflow? */ - return -EINVAL; - usec += plus; - if (usec > USEC_TIMESTAMP_FORMATTABLE_MAX) + usec = usec_add(usec, plus); + + if (usec < minus) return -EINVAL; - if (usec >= minus) - usec -= minus; - else + usec = usec_sub_unsigned(usec, minus); + + if (usec > USEC_TIMESTAMP_FORMATTABLE_MAX) return -EINVAL; if (ret) From 1d2c42c5dc765c57b4fba6b7c629093aa20685a8 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 04:27:52 +0900 Subject: [PATCH 11/15] time-util: shorten code a bit No functional change, just refactoring. --- src/basic/time-util.c | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 5ffd6bc5749..725bfe9fed6 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -634,8 +634,9 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { const char *k, *utc = NULL, *tzn = NULL; struct tm tm, copy; time_t x; - usec_t usec, x_usec, plus = 0, minus = 0; + usec_t usec, plus = 0, minus = 0; int r, weekday = -1, dst = -1; + unsigned fractional = 0; /* Allowed syntaxes: * @@ -751,7 +752,6 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { } x = (time_t) (usec / USEC_PER_SEC); - x_usec = 0; if (!localtime_or_gmtime_r(&x, &tm, utc)) return -EINVAL; @@ -860,19 +860,12 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { return -EINVAL; parse_usec: - { - unsigned add; - - k++; - r = parse_fractional_part_u(&k, 6, &add); - if (r < 0) - return -EINVAL; - - if (*k) - return -EINVAL; - - x_usec = add; - } + k++; + r = parse_fractional_part_u(&k, 6, &fractional); + if (r < 0) + return -EINVAL; + if (*k != '\0') + return -EINVAL; from_tm: if (weekday >= 0 && tm.tm_wday != weekday) @@ -882,7 +875,7 @@ from_tm: if (x < 0) return -EINVAL; - usec = usec_add(x * USEC_PER_SEC, x_usec); + usec = usec_add(x * USEC_PER_SEC, fractional); finish: usec = usec_add(usec, plus); From a83c1baaeb510f1916d2d8cf0324d100708c7073 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 15 Feb 2023 13:46:50 +0900 Subject: [PATCH 12/15] time-util: rename variables No functional changes, just refactoring. --- src/basic/time-util.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 725bfe9fed6..ea381da8f97 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -633,10 +633,10 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { const char *k, *utc = NULL, *tzn = NULL; struct tm tm, copy; - time_t x; usec_t usec, plus = 0, minus = 0; - int r, weekday = -1, dst = -1; + int r, weekday = -1, isdst = -1; unsigned fractional = 0; + time_t sec; /* Allowed syntaxes: * @@ -745,18 +745,18 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { if (IN_SET(j, 0, 1)) { /* Found one of the two timezones specified. */ t = strndupa_safe(t, e - t - 1); - dst = j; + isdst = j; tzn = tzname[j]; } } } - x = (time_t) (usec / USEC_PER_SEC); + sec = (time_t) (usec / USEC_PER_SEC); - if (!localtime_or_gmtime_r(&x, &tm, utc)) + if (!localtime_or_gmtime_r(&sec, &tm, utc)) return -EINVAL; - tm.tm_isdst = dst; + tm.tm_isdst = isdst; if (!with_tz && tzn) tm.tm_zone = tzn; @@ -871,11 +871,11 @@ from_tm: if (weekday >= 0 && tm.tm_wday != weekday) return -EINVAL; - x = mktime_or_timegm(&tm, utc); - if (x < 0) + sec = mktime_or_timegm(&tm, utc); + if (sec < 0) return -EINVAL; - usec = usec_add(x * USEC_PER_SEC, fractional); + usec = usec_add(sec * USEC_PER_SEC, fractional); finish: usec = usec_add(usec, plus); From 97c5f7ba1f50fcd7b982b995b46692c8cad4afaa Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 15 Feb 2023 13:51:15 +0900 Subject: [PATCH 13/15] time-util: drop unnecessary assignment of timezone name As mktime() does not use timezone neme. --- src/basic/time-util.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index ea381da8f97..1206ecd1b20 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -631,7 +631,7 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { { "Sat", 6 }, }; - const char *k, *utc = NULL, *tzn = NULL; + const char *k, *utc = NULL; struct tm tm, copy; usec_t usec, plus = 0, minus = 0; int r, weekday = -1, isdst = -1; @@ -746,7 +746,6 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { /* Found one of the two timezones specified. */ t = strndupa_safe(t, e - t - 1); isdst = j; - tzn = tzname[j]; } } } @@ -757,8 +756,6 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { return -EINVAL; tm.tm_isdst = isdst; - if (!with_tz && tzn) - tm.tm_zone = tzn; if (streq(t, "today")) { tm.tm_sec = tm.tm_min = tm.tm_hour = 0; From 7a9afae6040af0417d893328cb44b622dcdcb94f Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 14 Feb 2023 03:39:15 +0900 Subject: [PATCH 14/15] time-util: make parse_timestamp() use the RFC-822/ISO 8601 standard timezone spec If the timezone is specified with a number e.g. +0900 or so, then let's parse the time as a UTC, and adjust it with the specified time shift. Otherwise, if an area has timezone change, e.g. --- Africa/Casablanca Sun Jun 17 01:59:59 2018 UT = Sun Jun 17 01:59:59 2018 +00 isdst=0 gmtoff=0 Africa/Casablanca Sun Jun 17 02:00:00 2018 UT = Sun Jun 17 03:00:00 2018 +01 isdst=1 gmtoff=3600 Africa/Casablanca Sun Oct 28 01:59:59 2018 UT = Sun Oct 28 02:59:59 2018 +01 isdst=1 gmtoff=3600 Africa/Casablanca Sun Oct 28 02:00:00 2018 UT = Sun Oct 28 03:00:00 2018 +01 isdst=0 gmtoff=3600 --- then we could not determine isdst from the timezone (+01 in the above) and mktime() will provide wrong results. Fixes #26370. --- src/basic/time-util.c | 174 +++++++++++++++++++++++++++--------------- 1 file changed, 112 insertions(+), 62 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 1206ecd1b20..2d2a507b203 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -610,7 +610,14 @@ char* format_timespan(char *buf, size_t l, usec_t t, usec_t accuracy) { return buf; } -static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { +static int parse_timestamp_impl( + const char *t, + bool with_tz, + bool utc, + int isdst, + long gmtoff, + usec_t *ret) { + static const struct { const char *name; const int nr; @@ -631,11 +638,11 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { { "Sat", 6 }, }; - const char *k, *utc = NULL; - struct tm tm, copy; usec_t usec, plus = 0, minus = 0; - int r, weekday = -1, isdst = -1; + int r, weekday = -1; unsigned fractional = 0; + const char *k; + struct tm tm, copy; time_t sec; /* Allowed syntaxes: @@ -708,46 +715,6 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) { goto finish; } - - /* See if the timestamp is suffixed with UTC */ - utc = endswith_no_case(t, " UTC"); - if (utc) - t = strndupa_safe(t, utc - t); - else { - const char *e = NULL; - int j; - - tzset(); - - /* See if the timestamp is suffixed by either the DST or non-DST local timezone. Note - * that we only support the local timezones here, nothing else. Not because we - * wouldn't want to, but simply because there are no nice APIs available to cover - * this. By accepting the local time zone strings, we make sure that all timestamps - * written by format_timestamp() can be parsed correctly, even though we don't - * support arbitrary timezone specifications. */ - - for (j = 0; j <= 1; j++) { - - if (isempty(tzname[j])) - continue; - - e = endswith_no_case(t, tzname[j]); - if (!e) - continue; - if (e == t) - continue; - if (e[-1] != ' ') - continue; - - break; - } - - if (IN_SET(j, 0, 1)) { - /* Found one of the two timezones specified. */ - t = strndupa_safe(t, e - t - 1); - isdst = j; - } - } } sec = (time_t) (usec / USEC_PER_SEC); @@ -865,9 +832,32 @@ parse_usec: return -EINVAL; from_tm: + assert(plus == 0); + assert(minus == 0); + if (weekday >= 0 && tm.tm_wday != weekday) return -EINVAL; + if (gmtoff < 0) { + plus = -gmtoff * USEC_PER_SEC; + + /* If gmtoff is negative, the string maye be too old to be parsed as UTC. + * E.g. 1969-12-31 23:00:00 -06 == 1970-01-01 05:00:00 UTC + * We assumed that gmtoff is in the range of -24:00…+24:00, hence the only date we need to + * handle here is 1969-12-31. So, let's shift the date with one day, then subtract the shift + * later. */ + if (tm.tm_year == 69 && tm.tm_mon == 11 && tm.tm_mday == 31) { + /* Thu 1970-01-01-00:00:00 */ + tm.tm_year = 70; + tm.tm_mon = 0; + tm.tm_mday = 1; + tm.tm_wday = 4; + tm.tm_yday = 0; + minus = USEC_PER_DAY; + } + } else + minus = gmtoff * USEC_PER_SEC; + sec = mktime_or_timegm(&tm, utc); if (sec < 0) return -EINVAL; @@ -890,24 +880,93 @@ finish: return 0; } +static int parse_timestamp_with_tz(const char *t, size_t len, bool utc, int isdst, long gmtoff, usec_t *ret) { + _cleanup_free_ char *buf = NULL; + + assert(t); + assert(len > 0); + + buf = strndup(t, len); + if (!buf) + return -ENOMEM; + + return parse_timestamp_impl(buf, /* with_tz = */ true, utc, isdst, gmtoff, ret); +} + +static int parse_timestamp_maybe_with_tz(const char *t, size_t len, bool valid_tz, usec_t *ret) { + assert(t); + assert(len > 0); + + tzset(); + + for (int j = 0; j <= 1; j++) { + if (isempty(tzname[j])) + continue; + + if (!streq(t + len + 1, tzname[j])) + continue; + + /* The specified timezone matches tzname[] of the local timezone. */ + return parse_timestamp_with_tz(t, len, /* utc = */ false, /* isdst = */ j, /* gmtoff = */ 0, ret); + } + + if (valid_tz) + /* We know that the specified timezone is a valid zoneinfo (e.g. Asia/Tokyo). So, simply drop + * the timezone and parse the remaining string as a local time. */ + return parse_timestamp_with_tz(t, len, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret); + + return parse_timestamp_impl(t, /* with_tz = */ false, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret); +} + typedef struct ParseTimestampResult { usec_t usec; int return_value; } ParseTimestampResult; int parse_timestamp(const char *t, usec_t *ret) { - char *last_space, *tz = NULL; ParseTimestampResult *shared, tmp; + const char *k, *tz, *space; + struct tm tm; int r; assert(t); - last_space = strrchr(t, ' '); - if (last_space != NULL && timezone_is_valid(last_space + 1, LOG_DEBUG)) - tz = last_space + 1; + space = strrchr(t, ' '); + if (!space) + return parse_timestamp_impl(t, /* with_tz = */ false, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret); - if (!tz || endswith_no_case(t, " UTC")) - return parse_timestamp_impl(t, ret, false); + /* The string starts with space. */ + if (space == t) + return -EINVAL; + + /* Shortcut, parse the string as UTC. */ + if (streq(space + 1, "UTC")) + return parse_timestamp_with_tz(t, space - t, /* utc = */ true, /* isdst = */ -1, /* gmtoff = */ 0, ret); + + /* If the timezone is compatible with RFC-822/ISO 8601 (e.g. +06, or -03:00) then parse the string as + * UTC and shift the result. */ + k = strptime(space + 1, "%z", &tm); + if (k && *k == '\0') { + /* glibc accepts gmtoff more than 24 hours, but we refuse it. */ + if ((usec_t) labs(tm.tm_gmtoff) > USEC_PER_DAY / USEC_PER_SEC) + return -EINVAL; + + return parse_timestamp_with_tz(t, space - t, /* utc = */ true, /* isdst = */ -1, /* gmtoff = */ tm.tm_gmtoff, ret); + } + + /* If the last word is not a timezone file (e.g. Asia/Tokyo), then let's check if it matches + * tzname[] of the local timezone, e.g. JST or CEST. */ + if (!timezone_is_valid(space + 1, LOG_DEBUG)) + return parse_timestamp_maybe_with_tz(t, space - t, /* valid_tz = */ false, ret); + + /* Shortcut. If the current $TZ is equivalent to the specified timezone, it is not necessary to fork + * the process. */ + tz = getenv("TZ"); + if (tz && *tz == ':' && streq(tz + 1, space + 1)) + return parse_timestamp_maybe_with_tz(t, space - t, /* valid_tz = */ true, ret); + + /* Otherwise, to avoid polluting the current environment variables, let's fork the process and set + * the specified timezone in the child process. */ shared = mmap(NULL, sizeof *shared, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); if (shared == MAP_FAILED) @@ -919,11 +978,10 @@ int parse_timestamp(const char *t, usec_t *ret) { return r; } if (r == 0) { - bool with_tz = true; - char *colon_tz; + const char *colon_tz; /* tzset(3) says $TZ should be prefixed with ":" if we reference timezone files */ - colon_tz = strjoina(":", tz); + colon_tz = strjoina(":", space + 1); if (setenv("TZ", colon_tz, 1) != 0) { shared->return_value = negative_errno(); @@ -932,15 +990,7 @@ int parse_timestamp(const char *t, usec_t *ret) { tzset(); - /* If there is a timezone that matches the tzname fields, leave the parsing to the implementation. - * Otherwise just cut it off. */ - with_tz = !STR_IN_SET(tz, tzname[0], tzname[1]); - - /* Cut off the timezone if we don't need it. */ - if (with_tz) - t = strndupa_safe(t, last_space - t); - - shared->return_value = parse_timestamp_impl(t, &shared->usec, with_tz); + shared->return_value = parse_timestamp_maybe_with_tz(t, space - t, /* valid_tz = */ true, &shared->usec); _exit(EXIT_SUCCESS); } From 8b51c41fd0796b1299f3b7f2f11eaf4efae8c2db Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sun, 12 Feb 2023 05:30:49 +0900 Subject: [PATCH 15/15] test: add tests for format_timestamp() and parse_timestamp() with various timezone --- src/test/test-time-util.c | 378 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 366 insertions(+), 12 deletions(-) diff --git a/src/test/test-time-util.c b/src/test/test-time-util.c index dee012fa2ef..d4ba1249772 100644 --- a/src/test/test-time-util.c +++ b/src/test/test-time-util.c @@ -1,6 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "dirent-util.h" #include "env-util.h" +#include "fd-util.h" +#include "fileio.h" #include "random-util.h" #include "serialize.h" #include "string-util.h" @@ -8,6 +11,8 @@ #include "tests.h" #include "time-util.h" +#define TRIAL 100u + TEST(parse_sec) { usec_t u; @@ -334,11 +339,11 @@ TEST(usec_sub_signed) { } TEST(format_timestamp) { - for (unsigned i = 0; i < 100; i++) { + for (unsigned i = 0; i < TRIAL; i++) { char buf[CONST_MAX(FORMAT_TIMESTAMP_MAX, FORMAT_TIMESPAN_MAX)]; usec_t x, y; - x = random_u64_range(2147483600 * USEC_PER_SEC) + 1; + x = random_u64_range(USEC_TIMESTAMP_FORMATTABLE_MAX - USEC_PER_SEC) + USEC_PER_SEC; assert_se(format_timestamp(buf, sizeof(buf), x)); log_debug("%s", buf); @@ -384,20 +389,91 @@ TEST(format_timestamp) { } } +static void test_format_timestamp_impl(usec_t x) { + bool success; + const char *xx, *yy; + usec_t y; + + xx = FORMAT_TIMESTAMP(x); + assert_se(xx); + assert_se(parse_timestamp(xx, &y) >= 0); + yy = FORMAT_TIMESTAMP(y); + assert_se(yy); + + success = (x / USEC_PER_SEC == y / USEC_PER_SEC) && streq(xx, yy); + log_full(success ? LOG_DEBUG : LOG_ERR, "@" USEC_FMT " → %s → @" USEC_FMT " → %s", x, xx, y, yy); + assert_se(x / USEC_PER_SEC == y / USEC_PER_SEC); + assert_se(streq(xx, yy)); +} + +static void test_format_timestamp_loop(void) { + test_format_timestamp_impl(USEC_PER_SEC); + + for (unsigned i = 0; i < TRIAL; i++) { + usec_t x; + + x = random_u64_range(USEC_TIMESTAMP_FORMATTABLE_MAX - USEC_PER_SEC) + USEC_PER_SEC; + test_format_timestamp_impl(x); + } +} + TEST(FORMAT_TIMESTAMP) { - for (unsigned i = 0; i < 100; i++) { - _cleanup_free_ char *buf; - usec_t x, y; + test_format_timestamp_loop(); +} - x = random_u64_range(2147483600 * USEC_PER_SEC) + 1; +static void test_format_timestamp_with_tz_one(const char *name1, const char *name2) { + _cleanup_free_ char *buf = NULL, *tz = NULL; + const char *name, *saved_tz; - /* strbuf() is to test the macro in an argument to a function call. */ - assert_se(buf = strdup(FORMAT_TIMESTAMP(x))); - log_debug("%s", buf); - assert_se(parse_timestamp(buf, &y) >= 0); - assert_se(x / USEC_PER_SEC == y / USEC_PER_SEC); + if (name2) + assert_se(buf = path_join(name1, name2)); + name = buf ?: name1; - assert_se(streq(FORMAT_TIMESTAMP(x), buf)); + if (!timezone_is_valid(name, LOG_DEBUG)) + return; + + log_info("/* %s(%s) */", __func__, name); + + saved_tz = getenv("TZ"); + + assert_se(tz = strjoin(":", name)); + assert_se(setenv("TZ", tz, 1) >= 0); + tzset(); + log_debug("%s: tzname[0]=%s, tzname[1]=%s", tz, strempty(tzname[0]), strempty(tzname[1])); + + test_format_timestamp_loop(); + + assert_se(set_unset_env("TZ", saved_tz, true) == 0); + tzset(); +} + +TEST(FORMAT_TIMESTAMP_with_tz) { + if (!slow_tests_enabled()) + return (void) log_tests_skipped("slow tests are disabled"); + + _cleanup_closedir_ DIR *dir = opendir("/usr/share/zoneinfo"); + if (!dir) + return (void) log_tests_skipped_errno(errno, "Failed to open /usr/share/zoneinfo"); + + FOREACH_DIRENT(de, dir, break) { + if (de->d_type == DT_REG) + test_format_timestamp_with_tz_one(de->d_name, NULL); + + else if (de->d_type == DT_DIR) { + if (streq(de->d_name, "right")) + /* The test does not support timezone with leap second info. */ + continue; + + _cleanup_closedir_ DIR *subdir = xopendirat(dirfd(dir), de->d_name, 0); + if (!subdir) { + log_notice_errno(errno, "Failed to open /usr/share/zoneinfo/%s, ignoring: %m", de->d_name); + continue; + } + + FOREACH_DIRENT(subde, subdir, break) + if (subde->d_type == DT_REG) + test_format_timestamp_with_tz_one(de->d_name, subde->d_name); + } } } @@ -579,6 +655,219 @@ TEST(format_timestamp_range) { test_format_timestamp_one(USEC_INFINITY, TIMESTAMP_UTC, NULL); } +static void test_parse_timestamp_one(const char *str, usec_t max_diff, usec_t expected) { + usec_t usec; + + log_debug("/* %s(%s) */", __func__, str); + assert_se(parse_timestamp(str, &usec) >= 0); + assert_se(usec >= expected); + assert_se(usec_sub_unsigned(usec, expected) <= max_diff); +} + +TEST(parse_timestamp) { + usec_t today, now_usec; + + /* UTC */ + test_parse_timestamp_one("Thu 1970-01-01 00:01 UTC", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Thu 1970-01-01 00:00:01 UTC", 0, USEC_PER_SEC); + test_parse_timestamp_one("Thu 1970-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Thu 1970-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("Thu 70-01-01 00:01 UTC", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Thu 70-01-01 00:00:01 UTC", 0, USEC_PER_SEC); + test_parse_timestamp_one("Thu 70-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Thu 70-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("1970-01-01 00:01 UTC", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("1970-01-01 00:00:01 UTC", 0, USEC_PER_SEC); + test_parse_timestamp_one("1970-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("1970-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("70-01-01 00:01 UTC", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("70-01-01 00:00:01 UTC", 0, USEC_PER_SEC); + test_parse_timestamp_one("70-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("70-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000); + + if (timezone_is_valid("Asia/Tokyo", LOG_DEBUG)) { + /* Asia/Tokyo (+0900) */ + test_parse_timestamp_one("Thu 1970-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Thu 1970-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC); + test_parse_timestamp_one("Thu 1970-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Thu 1970-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("Thu 70-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Thu 70-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC); + test_parse_timestamp_one("Thu 70-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Thu 70-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("1970-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("1970-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC); + test_parse_timestamp_one("1970-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("1970-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("70-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("70-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC); + test_parse_timestamp_one("70-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("70-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000); + + const char *saved_tz = getenv("TZ"); + assert_se(setenv("TZ", ":Asia/Tokyo", 1) >= 0); + + /* JST (+0900) */ + test_parse_timestamp_one("Thu 1970-01-01 09:01 JST", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Thu 1970-01-01 09:00:01 JST", 0, USEC_PER_SEC); + test_parse_timestamp_one("Thu 1970-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Thu 1970-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("Thu 70-01-01 09:01 JST", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Thu 70-01-01 09:00:01 JST", 0, USEC_PER_SEC); + test_parse_timestamp_one("Thu 70-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Thu 70-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("1970-01-01 09:01 JST", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("1970-01-01 09:00:01 JST", 0, USEC_PER_SEC); + test_parse_timestamp_one("1970-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("1970-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("70-01-01 09:01 JST", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("70-01-01 09:00:01 JST", 0, USEC_PER_SEC); + test_parse_timestamp_one("70-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("70-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000); + + assert_se(set_unset_env("TZ", saved_tz, true) == 0); + } + + if (timezone_is_valid("America/New_York", LOG_DEBUG)) { + /* America/New_York (-0500) */ + test_parse_timestamp_one("Wed 1969-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Wed 1969-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC); + test_parse_timestamp_one("Wed 1969-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Wed 1969-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("Wed 69-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Wed 69-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC); + test_parse_timestamp_one("Wed 69-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Wed 69-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("1969-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("1969-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC); + test_parse_timestamp_one("1969-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("1969-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("69-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("69-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC); + test_parse_timestamp_one("69-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("69-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000); + + const char *saved_tz = getenv("TZ"); + assert_se(setenv("TZ", ":America/New_York", 1) >= 0); + + /* EST (-0500) */ + test_parse_timestamp_one("Wed 1969-12-31 19:01 EST", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Wed 1969-12-31 19:00:01 EST", 0, USEC_PER_SEC); + test_parse_timestamp_one("Wed 1969-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Wed 1969-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("Wed 69-12-31 19:01 EST", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Wed 69-12-31 19:00:01 EST", 0, USEC_PER_SEC); + test_parse_timestamp_one("Wed 69-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Wed 69-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("1969-12-31 19:01 EST", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("1969-12-31 19:00:01 EST", 0, USEC_PER_SEC); + test_parse_timestamp_one("1969-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("1969-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("69-12-31 19:01 EST", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("69-12-31 19:00:01 EST", 0, USEC_PER_SEC); + test_parse_timestamp_one("69-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("69-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000); + + assert_se(set_unset_env("TZ", saved_tz, true) == 0); + } + + /* -06 */ + test_parse_timestamp_one("Wed 1969-12-31 18:01 -06", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Wed 1969-12-31 18:00:01 -06", 0, USEC_PER_SEC); + test_parse_timestamp_one("Wed 1969-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Wed 1969-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("Wed 69-12-31 18:01 -06", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Wed 69-12-31 18:00:01 -06", 0, USEC_PER_SEC); + test_parse_timestamp_one("Wed 69-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Wed 69-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("1969-12-31 18:01 -06", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("1969-12-31 18:00:01 -06", 0, USEC_PER_SEC); + test_parse_timestamp_one("1969-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("1969-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("69-12-31 18:01 -06", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("69-12-31 18:00:01 -06", 0, USEC_PER_SEC); + test_parse_timestamp_one("69-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("69-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000); + + /* -0600 */ + test_parse_timestamp_one("Wed 1969-12-31 18:01 -0600", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Wed 1969-12-31 18:00:01 -0600", 0, USEC_PER_SEC); + test_parse_timestamp_one("Wed 1969-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Wed 1969-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("Wed 69-12-31 18:01 -0600", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Wed 69-12-31 18:00:01 -0600", 0, USEC_PER_SEC); + test_parse_timestamp_one("Wed 69-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Wed 69-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("1969-12-31 18:01 -0600", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("1969-12-31 18:00:01 -0600", 0, USEC_PER_SEC); + test_parse_timestamp_one("1969-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("1969-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("69-12-31 18:01 -0600", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("69-12-31 18:00:01 -0600", 0, USEC_PER_SEC); + test_parse_timestamp_one("69-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("69-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000); + + /* -06:00 */ + test_parse_timestamp_one("Wed 1969-12-31 18:01 -06:00", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Wed 1969-12-31 18:00:01 -06:00", 0, USEC_PER_SEC); + test_parse_timestamp_one("Wed 1969-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Wed 1969-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("Wed 69-12-31 18:01 -06:00", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("Wed 69-12-31 18:00:01 -06:00", 0, USEC_PER_SEC); + test_parse_timestamp_one("Wed 69-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("Wed 69-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("1969-12-31 18:01 -06:00", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("1969-12-31 18:00:01 -06:00", 0, USEC_PER_SEC); + test_parse_timestamp_one("1969-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("1969-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000); + + test_parse_timestamp_one("69-12-31 18:01 -06:00", 0, USEC_PER_MINUTE); + test_parse_timestamp_one("69-12-31 18:00:01 -06:00", 0, USEC_PER_SEC); + test_parse_timestamp_one("69-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000); + test_parse_timestamp_one("69-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000); + + /* without date */ + assert_se(parse_timestamp("today", &today) == 0); + test_parse_timestamp_one("00:01", 0, today + USEC_PER_MINUTE); + test_parse_timestamp_one("00:00:01", 0, today + USEC_PER_SEC); + test_parse_timestamp_one("00:00:01.001", 0, today + USEC_PER_SEC + 1000); + test_parse_timestamp_one("00:00:01.0010", 0, today + USEC_PER_SEC + 1000); + test_parse_timestamp_one("tomorrow", 0, today + USEC_PER_DAY); + test_parse_timestamp_one("yesterday", 0, today - USEC_PER_DAY); + + /* relative */ + assert_se(parse_timestamp("now", &now_usec) == 0); + test_parse_timestamp_one("+5hours", USEC_PER_MINUTE, now_usec + 5 * USEC_PER_HOUR); + if (now_usec >= 10 * USEC_PER_DAY) + test_parse_timestamp_one("-10days", USEC_PER_MINUTE, now_usec - 10 * USEC_PER_DAY); + test_parse_timestamp_one("2weeks left", USEC_PER_MINUTE, now_usec + 2 * USEC_PER_WEEK); + if (now_usec >= 30 * USEC_PER_MINUTE) + test_parse_timestamp_one("30minutes ago", USEC_PER_MINUTE, now_usec - 30 * USEC_PER_MINUTE); +} + TEST(deserialize_dual_timestamp) { int r; dual_timestamp t; @@ -702,6 +991,71 @@ TEST(map_clock_usec) { } } +static void test_timezone_offset_change_one(const char *utc, const char *pretty) { + usec_t x, y, z; + char *s; + + assert_se(parse_timestamp(utc, &x) >= 0); + + s = FORMAT_TIMESTAMP_STYLE(x, TIMESTAMP_UTC); + assert_se(parse_timestamp(s, &y) >= 0); + log_debug("%s -> " USEC_FMT " -> %s -> " USEC_FMT, utc, x, s, y); + assert_se(streq(s, utc)); + assert_se(x == y); + + assert_se(parse_timestamp(pretty, &y) >= 0); + s = FORMAT_TIMESTAMP_STYLE(y, TIMESTAMP_PRETTY); + assert_se(parse_timestamp(s, &z) >= 0); + log_debug("%s -> " USEC_FMT " -> %s -> " USEC_FMT, pretty, y, s, z); + assert_se(streq(s, pretty)); + assert_se(x == y); + assert_se(x == z); +} + +TEST(timezone_offset_change) { + const char *tz = getenv("TZ"); + + /* See issue #26370. */ + + if (timezone_is_valid("Africa/Casablanca", LOG_DEBUG)) { + assert_se(setenv("TZ", ":Africa/Casablanca", 1) >= 0); + tzset(); + log_debug("Africa/Casablanca: tzname[0]=%s, tzname[1]=%s", strempty(tzname[0]), strempty(tzname[1])); + + test_timezone_offset_change_one("Sun 2015-10-25 01:59:59 UTC", "Sun 2015-10-25 02:59:59 +01"); + test_timezone_offset_change_one("Sun 2015-10-25 02:00:00 UTC", "Sun 2015-10-25 02:00:00 +00"); + test_timezone_offset_change_one("Sun 2018-06-17 01:59:59 UTC", "Sun 2018-06-17 01:59:59 +00"); + test_timezone_offset_change_one("Sun 2018-06-17 02:00:00 UTC", "Sun 2018-06-17 03:00:00 +01"); + test_timezone_offset_change_one("Sun 2018-10-28 01:59:59 UTC", "Sun 2018-10-28 02:59:59 +01"); + test_timezone_offset_change_one("Sun 2018-10-28 02:00:00 UTC", "Sun 2018-10-28 03:00:00 +01"); + } + + if (timezone_is_valid("Asia/Atyrau", LOG_DEBUG)) { + assert_se(setenv("TZ", ":Asia/Atyrau", 1) >= 0); + tzset(); + log_debug("Asia/Atyrau: tzname[0]=%s, tzname[1]=%s", strempty(tzname[0]), strempty(tzname[1])); + + test_timezone_offset_change_one("Sat 2004-03-27 21:59:59 UTC", "Sun 2004-03-28 01:59:59 +04"); + test_timezone_offset_change_one("Sat 2004-03-27 22:00:00 UTC", "Sun 2004-03-28 03:00:00 +05"); + test_timezone_offset_change_one("Sat 2004-10-30 21:59:59 UTC", "Sun 2004-10-31 02:59:59 +05"); + test_timezone_offset_change_one("Sat 2004-10-30 22:00:00 UTC", "Sun 2004-10-31 03:00:00 +05"); + } + + if (timezone_is_valid("Chile/EasterIsland", LOG_DEBUG)) { + assert_se(setenv("TZ", ":Chile/EasterIsland", 1) >= 0); + tzset(); + log_debug("Chile/EasterIsland: tzname[0]=%s, tzname[1]=%s", strempty(tzname[0]), strempty(tzname[1])); + + test_timezone_offset_change_one("Sun 1981-10-11 03:59:59 UTC", "Sat 1981-10-10 20:59:59 -07"); + test_timezone_offset_change_one("Sun 1981-10-11 04:00:00 UTC", "Sat 1981-10-10 22:00:00 -06"); + test_timezone_offset_change_one("Sun 1982-03-14 02:59:59 UTC", "Sat 1982-03-13 20:59:59 -06"); + test_timezone_offset_change_one("Sun 1982-03-14 03:00:00 UTC", "Sat 1982-03-13 21:00:00 -06"); + } + + assert_se(set_unset_env("TZ", tz, true) == 0); + tzset(); +} + static int intro(void) { log_info("realtime=" USEC_FMT "\n" "monotonic=" USEC_FMT "\n"