diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 2d2a507b203..26fd1bae653 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -612,7 +612,7 @@ char* format_timespan(char *buf, size_t l, usec_t t, usec_t accuracy) { static int parse_timestamp_impl( const char *t, - bool with_tz, + size_t tz_offset, bool utc, int isdst, long gmtoff, @@ -638,7 +638,9 @@ static int parse_timestamp_impl( { "Sat", 6 }, }; + _cleanup_free_ char *t_alloc = NULL; usec_t usec, plus = 0, minus = 0; + bool with_tz = false; int r, weekday = -1; unsigned fractional = 0; const char *k; @@ -663,6 +665,29 @@ static int parse_timestamp_impl( assert(t); + if (tz_offset != SIZE_MAX) { + /* If the input string contains timezone, then cut it here. */ + + if (tz_offset <= 1) /* timezone must be after a space. */ + return -EINVAL; + + t_alloc = strndup(t, tz_offset - 1); + if (!t_alloc) + return -ENOMEM; + + t = t_alloc; + with_tz = true; + } + + if (utc) { + /* glibc accepts gmtoff more than 24 hours, but we refuse it. */ + if ((usec_t) labs(gmtoff) * USEC_PER_SEC > USEC_PER_DAY) + return -EINVAL; + } else { + if (gmtoff != 0) + return -EINVAL; + } + if (t[0] == '@' && !with_tz) return parse_sec(t + 1, ret); @@ -841,7 +866,7 @@ from_tm: if (gmtoff < 0) { plus = -gmtoff * USEC_PER_SEC; - /* If gmtoff is negative, the string maye be too old to be parsed as UTC. + /* If gmtoff is negative, the string may 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 @@ -880,22 +905,8 @@ 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; - +static int parse_timestamp_maybe_with_tz(const char *t, size_t tz_offset, bool valid_tz, usec_t *ret) { 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(); @@ -903,19 +914,18 @@ static int parse_timestamp_maybe_with_tz(const char *t, size_t len, bool valid_t if (isempty(tzname[j])) continue; - if (!streq(t + len + 1, tzname[j])) + if (!streq(t + tz_offset, 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); + return parse_timestamp_impl(t, tz_offset, /* 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); + /* If we know that the last word is a valid timezone (e.g. Asia/Tokyo), then simply drop the timezone + * and parse the remaining string as a local time. If we know that the last word is not a timezone, + * then assume that it is a part of the time and try to parse the whole string as a local time. */ + return parse_timestamp_impl(t, valid_tz ? tz_offset : SIZE_MAX, + /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret); } typedef struct ParseTimestampResult { @@ -925,45 +935,41 @@ typedef struct ParseTimestampResult { int parse_timestamp(const char *t, usec_t *ret) { ParseTimestampResult *shared, tmp; - const char *k, *tz, *space; + const char *k, *tz, *current_tz; + size_t tz_offset; struct tm tm; int r; assert(t); - space = strrchr(t, ' '); - if (!space) - return parse_timestamp_impl(t, /* with_tz = */ false, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret); + tz = strrchr(t, ' '); + if (!tz) + return parse_timestamp_impl(t, /* tz_offset = */ SIZE_MAX, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret); - /* The string starts with space. */ - if (space == t) - return -EINVAL; + tz++; + tz_offset = tz - t; /* 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 (streq(tz, "UTC")) + return parse_timestamp_impl(t, tz_offset, /* 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); - } + * UTC and shift the result. Note, this must be earlier than the timezone check with tzname[], as + * tzname[] may be in the same format. */ + k = strptime(tz, "%z", &tm); + if (k && *k == '\0') + return parse_timestamp_impl(t, tz_offset, /* 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); + if (!timezone_is_valid(tz, LOG_DEBUG)) + return parse_timestamp_maybe_with_tz(t, tz_offset, /* 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); + current_tz = getenv("TZ"); + if (current_tz && *current_tz == ':' && streq(current_tz + 1, tz)) + return parse_timestamp_maybe_with_tz(t, tz_offset, /* 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. */ @@ -981,7 +987,7 @@ int parse_timestamp(const char *t, usec_t *ret) { const char *colon_tz; /* tzset(3) says $TZ should be prefixed with ":" if we reference timezone files */ - colon_tz = strjoina(":", space + 1); + colon_tz = strjoina(":", tz); if (setenv("TZ", colon_tz, 1) != 0) { shared->return_value = negative_errno(); @@ -990,7 +996,7 @@ int parse_timestamp(const char *t, usec_t *ret) { tzset(); - shared->return_value = parse_timestamp_maybe_with_tz(t, space - t, /* valid_tz = */ true, &shared->usec); + shared->return_value = parse_timestamp_maybe_with_tz(t, tz_offset, /* valid_tz = */ true, &shared->usec); _exit(EXIT_SUCCESS); } diff --git a/src/test/meson.build b/src/test/meson.build index dc9e95a7bdd..ae4c3f13aa8 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -156,7 +156,6 @@ simple_tests += files( 'test-strxcpyx.c', 'test-sysctl-util.c', 'test-terminal-util.c', - 'test-time-util.c', 'test-tmpfile-util.c', 'test-tmpfiles.c', 'test-tpm2.c', @@ -501,6 +500,10 @@ tests += [ udev_includes, ], }, + { + 'sources' : files('test-time-util.c'), + 'timeout' : 120, + }, { 'sources' : files('test-udev.c'), 'link_with' : [ diff --git a/src/test/test-time-util.c b/src/test/test-time-util.c index d6fbb8582f5..e974520bbe2 100644 --- a/src/test/test-time-util.c +++ b/src/test/test-time-util.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include "dirent-util.h" #include "env-util.h" #include "fd-util.h" #include "fileio.h" @@ -421,23 +420,18 @@ TEST(FORMAT_TIMESTAMP) { test_format_timestamp_loop(); } -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; +static void test_format_timestamp_with_tz_one(const char *tz) { + const char *saved_tz, *colon_tz; - if (name2) - assert_se(buf = path_join(name1, name2)); - name = buf ?: name1; - - if (!timezone_is_valid(name, LOG_DEBUG)) + if (!timezone_is_valid(tz, LOG_DEBUG)) return; - log_info("/* %s(%s) */", __func__, name); + log_info("/* %s(%s) */", __func__, tz); saved_tz = getenv("TZ"); - assert_se(tz = strjoin(":", name)); - assert_se(setenv("TZ", tz, 1) >= 0); + assert_se(colon_tz = strjoina(":", tz)); + assert_se(setenv("TZ", colon_tz, 1) >= 0); tzset(); log_debug("%s: tzname[0]=%s, tzname[1]=%s", tz, strempty(tzname[0]), strempty(tzname[1])); @@ -448,33 +442,11 @@ static void test_format_timestamp_with_tz_one(const char *name1, const char *nam } TEST(FORMAT_TIMESTAMP_with_tz) { - if (!slow_tests_enabled()) - return (void) log_tests_skipped("slow tests are disabled"); + _cleanup_strv_free_ char **timezones = NULL; - _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); - } - } + assert_se(get_timezones(&timezones) >= 0); + STRV_FOREACH(tz, timezones) + test_format_timestamp_with_tz_one(*tz); } TEST(format_timestamp_relative_full) { @@ -656,15 +628,17 @@ TEST(format_timestamp_range) { } static void test_parse_timestamp_one(const char *str, usec_t max_diff, usec_t expected) { - usec_t usec; + usec_t usec = USEC_INFINITY; + int r; - log_debug("/* %s(%s) */", __func__, str); - assert_se(parse_timestamp(str, &usec) >= 0); + r = parse_timestamp(str, &usec); + log_debug("/* %s(%s): max_diff="USEC_FMT", expected="USEC_FMT", result="USEC_FMT"*/", __func__, str, max_diff, expected, usec); + assert_se(r >= 0); assert_se(usec >= expected); assert_se(usec_sub_unsigned(usec, expected) <= max_diff); } -TEST(parse_timestamp) { +static void test_parse_timestamp_impl(const char *tz) { usec_t today, now_usec; /* UTC */ @@ -709,10 +683,9 @@ TEST(parse_timestamp) { 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); - + if (streq_ptr(tz, "Asia/Tokyo")) { /* 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); @@ -733,8 +706,6 @@ TEST(parse_timestamp) { 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)) { @@ -758,10 +729,9 @@ TEST(parse_timestamp) { 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); - + if (streq_ptr(tz, "America/New_York")) { /* 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); @@ -782,8 +752,6 @@ TEST(parse_timestamp) { 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 */ @@ -851,13 +819,12 @@ TEST(parse_timestamp) { /* without date */ assert_se(parse_timestamp("today", &today) == 0); - // FIXME: currently failing, needs to investigate the changes from https://github.com/systemd/systemd/pull/26409 - /*test_parse_timestamp_one("00:01", 0, today + USEC_PER_MINUTE); + 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);*/ + test_parse_timestamp_one("yesterday", 0, today - USEC_PER_DAY); /* relative */ assert_se(parse_timestamp("now", &now_usec) == 0); @@ -869,6 +836,39 @@ TEST(parse_timestamp) { test_parse_timestamp_one("30minutes ago", USEC_PER_MINUTE, now_usec - 30 * USEC_PER_MINUTE); } +TEST(parse_timestamp) { + test_parse_timestamp_impl(NULL); +} + +static void test_parse_timestamp_with_tz_one(const char *tz) { + const char *saved_tz, *colon_tz; + + if (!timezone_is_valid(tz, LOG_DEBUG)) + return; + + log_info("/* %s(%s) */", __func__, tz); + + saved_tz = getenv("TZ"); + + assert_se(colon_tz = strjoina(":", tz)); + assert_se(setenv("TZ", colon_tz, 1) >= 0); + tzset(); + log_debug("%s: tzname[0]=%s, tzname[1]=%s", tz, strempty(tzname[0]), strempty(tzname[1])); + + test_parse_timestamp_impl(tz); + + assert_se(set_unset_env("TZ", saved_tz, true) == 0); + tzset(); +} + +TEST(parse_timestamp_with_tz) { + _cleanup_strv_free_ char **timezones = NULL; + + assert_se(get_timezones(&timezones) >= 0); + STRV_FOREACH(tz, timezones) + test_parse_timestamp_with_tz_one(*tz); +} + TEST(deserialize_dual_timestamp) { int r; dual_timestamp t;