diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 43ef5e4270..fb54ba276c 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -7,6 +7,7 @@ #include #include "alloc-util.h" +#include "env-util.h" #include "errno-util.h" #include "extract-word.h" #include "fd-util.h" @@ -623,6 +624,19 @@ char* format_timespan(char *buf, size_t l, usec_t t, usec_t accuracy) { return buf; } +int parse_gmtoff(const char *t, long *ret) { + assert(t); + + struct tm tm; + const char *k = strptime(t, "%z", &tm); + if (!k || *k != '\0') + return -EINVAL; + + if (ret) + *ret = tm.tm_gmtoff; + return 0; +} + static int parse_timestamp_impl( const char *t, size_t max_len, @@ -952,44 +966,13 @@ finish: return 0; } -static int parse_timestamp_maybe_with_tz(const char *t, size_t tz_offset, bool valid_tz, usec_t *ret) { - assert(t); - - tzset(); - - for (int j = 0; j <= 1; j++) { - if (isempty(tzname[j])) - continue; - - if (!streq(t + tz_offset, tzname[j])) - continue; - - /* The specified timezone matches tzname[] of the local timezone. */ - return parse_timestamp_impl(t, tz_offset - 1, /* utc = */ false, /* isdst = */ j, /* 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 - 1 : SIZE_MAX, - /* 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) { - ParseTimestampResult *shared, tmp; - const char *k, *tz, *current_tz; - size_t max_len, t_len; - struct tm tm; + long gmtoff; int r; assert(t); - t_len = strlen(t); + size_t t_len = strlen(t); if (t_len > 2 && t[t_len - 1] == 'Z') { /* Try to parse as RFC3339-style welded UTC: "1985-04-12T23:20:50.52Z" */ r = parse_timestamp_impl(t, t_len - 1, /* utc = */ true, /* isdst = */ -1, /* gmtoff = */ 0, ret); @@ -997,17 +980,15 @@ int parse_timestamp(const char *t, usec_t *ret) { return r; } - if (t_len > 7 && IN_SET(t[t_len - 6], '+', '-') && t[t_len - 7] != ' ') { /* RFC3339-style welded offset: "1990-12-31T15:59:60-08:00" */ - k = strptime(&t[t_len - 6], "%z", &tm); - if (k && *k == '\0') - return parse_timestamp_impl(t, t_len - 6, /* utc = */ true, /* isdst = */ -1, /* gmtoff = */ tm.tm_gmtoff, ret); - } + /* RFC3339-style welded offset: "1990-12-31T15:59:60-08:00" */ + if (t_len > 7 && IN_SET(t[t_len - 6], '+', '-') && t[t_len - 7] != ' ' && parse_gmtoff(&t[t_len - 6], &gmtoff) >= 0) + return parse_timestamp_impl(t, t_len - 6, /* utc = */ true, /* isdst = */ -1, gmtoff, ret); - tz = strrchr(t, ' '); + const char *tz = strrchr(t, ' '); if (!tz) return parse_timestamp_impl(t, /* max_len = */ SIZE_MAX, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret); - max_len = tz - t; + size_t max_len = tz - t; tz++; /* Shortcut, parse the string as UTC. */ @@ -1017,65 +998,39 @@ int parse_timestamp(const char *t, usec_t *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. 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, max_len, /* utc = */ true, /* isdst = */ -1, /* gmtoff = */ tm.tm_gmtoff, ret); + if (parse_gmtoff(tz, &gmtoff) >= 0) + return parse_timestamp_impl(t, max_len, /* utc = */ true, /* isdst = */ -1, 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(tz, LOG_DEBUG)) - return parse_timestamp_maybe_with_tz(t, tz - t, /* valid_tz = */ false, ret); + /* Check if the last word matches tzname[] of the local timezone. Note, this must be done earlier + * than the check by timezone_is_valid() below, as some short timezone specifications have their own + * timezone files (e.g. WET has its own timezone file, but JST does not), but using such files does + * not follow the timezone change in the current area. */ + tzset(); + for (int j = 0; j <= 1; j++) { + if (isempty(tzname[j])) + continue; - /* Shortcut. If the current $TZ is equivalent to the specified timezone, it is not necessary to fork - * the process. */ - current_tz = getenv("TZ"); - if (current_tz && *current_tz == ':' && streq(current_tz + 1, tz)) - return parse_timestamp_maybe_with_tz(t, tz - t, /* valid_tz = */ true, ret); + if (!streq(tz, tzname[j])) + continue; - /* 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) - return negative_errno(); - - /* The input string may be in argv. Let's copy it. */ - _cleanup_free_ char *t_copy = strdup(t); - if (!t_copy) - return -ENOMEM; - - t = t_copy; - assert_se(tz = endswith(t_copy, tz)); - - r = safe_fork("(sd-timestamp)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGKILL|FORK_WAIT, NULL); - if (r < 0) { - (void) munmap(shared, sizeof *shared); - return r; - } - if (r == 0) { - const char *colon_tz; - - /* tzset(3) says $TZ should be prefixed with ":" if we reference timezone files */ - colon_tz = strjoina(":", tz); - - if (setenv("TZ", colon_tz, 1) != 0) { - shared->return_value = negative_errno(); - _exit(EXIT_FAILURE); - } - - shared->return_value = parse_timestamp_maybe_with_tz(t, tz - t, /* valid_tz = */ true, &shared->usec); - - _exit(EXIT_SUCCESS); + /* The specified timezone matches tzname[] of the local timezone. */ + return parse_timestamp_impl(t, max_len, /* utc = */ false, /* isdst = */ j, /* gmtoff = */ 0, ret); } - tmp = *shared; - if (munmap(shared, sizeof *shared) != 0) - return negative_errno(); + /* If the last word is a valid timezone file (e.g. Asia/Tokyo), then save the current timezone, apply + * the specified timezone, and parse the remaining string as a local time. */ + if (timezone_is_valid(tz, LOG_DEBUG)) { + SAVE_TIMEZONE; - if (tmp.return_value == 0 && ret) - *ret = tmp.usec; + if (setenv("TZ", strjoina(":", tz), /* overwrite = */ true) < 0) + return negative_errno(); - return tmp.return_value; + return parse_timestamp_impl(t, max_len, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret); + } + + /* Otherwise, assume that the last word is a part of the time and try to parse the whole string as a + * local time. */ + return parse_timestamp_impl(t, SIZE_MAX, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret); } static const char* extract_multiplier(const char *p, usec_t *ret) { @@ -1592,6 +1547,26 @@ int verify_timezone(const char *name, int log_level) { return 0; } +void reset_timezonep(char **p) { + assert(p); + + (void) set_unset_env("TZ", *p, /* overwrite = */ true); + tzset(); + *p = mfree(*p); +} + +char* save_timezone(void) { + const char *e = getenv("TZ"); + if (!e) + return NULL; + + char *s = strdup(e); + if (!s) + log_debug("Failed to save $TZ=%s, unsetting the environment variable.", e); + + return s; +} + bool clock_supported(clockid_t clock) { struct timespec ts; diff --git a/src/basic/time-util.h b/src/basic/time-util.h index d31e62d18a..0c698eabd8 100644 --- a/src/basic/time-util.h +++ b/src/basic/time-util.h @@ -149,6 +149,7 @@ 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_gmtoff(const char *t, long *ret); int parse_timestamp(const char *t, usec_t *ret); int parse_sec(const char *t, usec_t *ret); @@ -163,6 +164,12 @@ static inline bool timezone_is_valid(const char *name, int log_level) { return verify_timezone(name, log_level) >= 0; } +void reset_timezonep(char **p); +char* save_timezone(void); +#define SAVE_TIMEZONE \ + _unused_ _cleanup_(reset_timezonep) \ + char *_saved_timezone_ = save_timezone() + bool clock_supported(clockid_t clock); usec_t usec_shift_clock(usec_t, clockid_t from, clockid_t to); diff --git a/src/test/test-time-util.c b/src/test/test-time-util.c index 5e01bb2fe3..7569f89ca0 100644 --- a/src/test/test-time-util.c +++ b/src/test/test-time-util.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include + #include "env-util.h" #include "random-util.h" #include "serialize.h" @@ -11,6 +12,18 @@ #define TRIAL 100u +static void set_timezone(const char *tz) { + if (!tz) + ASSERT_OK_ERRNO(unsetenv("TZ")); + if (isempty(tz)) + ASSERT_OK_ERRNO(setenv("TZ", tz, /* overwrite = */ true)); + else + ASSERT_OK_ERRNO(setenv("TZ", strjoina(":", tz), /* overwrite = */ true)); + + tzset(); + log_info("TZ=%s, tzname[0]=%s, tzname[1]=%s", strna(getenv("TZ")), strempty(tzname[0]), strempty(tzname[1])); +} + TEST(parse_sec) { usec_t u; @@ -390,35 +403,33 @@ TEST(format_timestamp) { } static void test_format_timestamp_impl(usec_t x) { - bool success, override; - const char *xx, *yy; - usec_t y, x_sec, y_sec; - - xx = FORMAT_TIMESTAMP(x); + const char *xx = FORMAT_TIMESTAMP(x); ASSERT_NOT_NULL(xx); + + usec_t y; ASSERT_OK(parse_timestamp(xx, &y)); - yy = FORMAT_TIMESTAMP(y); + const char *yy = FORMAT_TIMESTAMP(y); ASSERT_NOT_NULL(yy); - x_sec = x / USEC_PER_SEC; - y_sec = y / USEC_PER_SEC; - success = (x_sec == y_sec) && streq(xx, yy); - /* Workaround for https://github.com/systemd/systemd/issues/28472 - * and https://github.com/systemd/systemd/pull/35471. */ - override = !success && - (STRPTR_IN_SET(tzname[0], "CAT", "EAT", "WET") || - STRPTR_IN_SET(tzname[1], "CAT", "EAT", "WET")) && - (x_sec > y_sec ? x_sec - y_sec : y_sec - x_sec) == 3600; /* 1 hour, ignore fractional second */ - log_full(success ? LOG_DEBUG : override ? LOG_WARNING : LOG_ERR, + usec_t x_sec = x / USEC_PER_SEC; + usec_t y_sec = y / USEC_PER_SEC; + + if (x_sec == y_sec && streq(xx, yy)) + return; /* Yay!*/ + + /* When the timezone is built with rearguard being enabled (e.g. old Ubuntu and RHEL), the following + * timezone may provide time shifted 1 hour from the original. See + * https://github.com/systemd/systemd/issues/28472 and https://github.com/systemd/systemd/pull/35471 */ + bool ignore = + streq_ptr(getenv("TZ"), ":Africa/Windhoek") && + (x_sec > y_sec ? x_sec - y_sec : y_sec - x_sec) == 3600; + + log_full(ignore ? LOG_WARNING : LOG_ERR, "@" USEC_FMT " → %s → @" USEC_FMT " → %s%s", x, xx, y, yy, - override ? ", ignoring." : ""); - if (!override) { - if (!success) - log_warning("tzname[0]=\"%s\", tzname[1]=\"%s\"", tzname[0], tzname[1]); - ASSERT_EQ(x_sec, y_sec); - ASSERT_STREQ(xx, yy); - } + ignore ? ", ignoring." : ""); + + ASSERT_TRUE(ignore); } static void test_format_timestamp_loop(void) { @@ -445,24 +456,13 @@ TEST(FORMAT_TIMESTAMP) { } static void test_format_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])); + SAVE_TIMEZONE; + set_timezone(tz); test_format_timestamp_loop(); - - assert_se(set_unset_env("TZ", saved_tz, true) == 0); - tzset(); } TEST(FORMAT_TIMESTAMP_with_tz) { @@ -661,6 +661,35 @@ TEST(format_timestamp_range) { test_format_timestamp_one(USEC_INFINITY, TIMESTAMP_UTC, NULL); } +TEST(parse_gmtoff) { + long t; + + ASSERT_OK(parse_gmtoff("+14", &t)); + ASSERT_EQ(t, (long) (14 * USEC_PER_HOUR / USEC_PER_SEC)); + ASSERT_OK(parse_gmtoff("-09", &t)); + ASSERT_EQ(t, - (long) (9 * USEC_PER_HOUR / USEC_PER_SEC)); + ASSERT_OK(parse_gmtoff("+1400", &t)); + ASSERT_EQ(t, (long) (14 * USEC_PER_HOUR / USEC_PER_SEC)); + ASSERT_OK(parse_gmtoff("-0900", &t)); + ASSERT_EQ(t, - (long) (9 * USEC_PER_HOUR / USEC_PER_SEC)); + ASSERT_OK(parse_gmtoff("+14:00", &t)); + ASSERT_EQ(t, (long) (14 * USEC_PER_HOUR / USEC_PER_SEC)); + ASSERT_OK(parse_gmtoff("-09:00", &t)); + ASSERT_EQ(t, - (long) (9 * USEC_PER_HOUR / USEC_PER_SEC)); + + ASSERT_ERROR(parse_gmtoff("", &t), EINVAL); + ASSERT_ERROR(parse_gmtoff("UTC", &t), EINVAL); + ASSERT_ERROR(parse_gmtoff("09", &t), EINVAL); + ASSERT_ERROR(parse_gmtoff("0900", &t), EINVAL); + ASSERT_ERROR(parse_gmtoff("?0900", &t), EINVAL); + ASSERT_ERROR(parse_gmtoff("?0900", &t), EINVAL); + ASSERT_ERROR(parse_gmtoff("+0900abc", &t), EINVAL); + ASSERT_ERROR(parse_gmtoff("+0900 ", &t), EINVAL); + ASSERT_ERROR(parse_gmtoff("+090000", &t), EINVAL); + ASSERT_ERROR(parse_gmtoff("+0900:00", &t), EINVAL); + ASSERT_ERROR(parse_gmtoff("+0900.00", &t), EINVAL); +} + static void test_parse_timestamp_one(const char *str, usec_t max_diff, usec_t expected) { usec_t usec = USEC_INFINITY; int r; @@ -984,24 +1013,13 @@ TEST(parse_timestamp) { } 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])); + SAVE_TIMEZONE; + set_timezone(tz); test_parse_timestamp_impl(tz); - - assert_se(set_unset_env("TZ", saved_tz, true) == 0); - tzset(); } TEST(parse_timestamp_with_tz) { @@ -1093,7 +1111,7 @@ TEST(usec_shift_clock) { } TEST(in_utc_timezone) { - const char *tz = getenv("TZ"); + SAVE_TIMEZONE; assert_se(setenv("TZ", ":UTC", 1) >= 0); assert_se(in_utc_timezone()); @@ -1106,9 +1124,6 @@ TEST(in_utc_timezone) { assert_se(!in_utc_timezone()); ASSERT_STREQ(tzname[0], "CET"); ASSERT_STREQ(tzname[1], "CEST"); - - assert_se(set_unset_env("TZ", tz, true) == 0); - tzset(); } TEST(map_clock_usec) { @@ -1162,14 +1177,12 @@ static void test_timezone_offset_change_one(const char *utc, const char *pretty) } TEST(timezone_offset_change) { - const char *tz = getenv("TZ"); + SAVE_TIMEZONE; /* 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])); + set_timezone("Africa/Casablanca"); 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"); @@ -1180,9 +1193,7 @@ TEST(timezone_offset_change) { } 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])); + set_timezone("Asia/Atyrau"); 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"); @@ -1191,18 +1202,13 @@ TEST(timezone_offset_change) { } 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])); + set_timezone("Chile/EasterIsland"); 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 usec_t absdiff(usec_t a, usec_t b) {