From e3f561a6247579360099a25d03155f75ca428b9b Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 9 Sep 2025 09:36:07 +0900 Subject: [PATCH 1/3] time-util: introduce SAVE_TIMEZONE macro --- src/basic/time-util.c | 21 +++++++++++++ src/basic/time-util.h | 6 ++++ src/test/test-time-util.c | 65 +++++++++++++-------------------------- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 43ef5e4270..bb5ba210b3 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" @@ -1592,6 +1593,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..da31e29f12 100644 --- a/src/basic/time-util.h +++ b/src/basic/time-util.h @@ -163,6 +163,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..b036f6aba5 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; @@ -445,24 +458,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) { @@ -984,24 +986,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 +1084,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 +1097,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 +1150,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 +1166,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 +1175,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) { From 23407c1806b037ce627a60d60df85a824b23967e Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 9 Sep 2025 09:50:46 +0900 Subject: [PATCH 2/3] time-util: introduce parse_gmtoff() helper function --- src/basic/time-util.c | 13 +++++++++++++ src/basic/time-util.h | 1 + src/test/test-time-util.c | 29 +++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index bb5ba210b3..fa24d79d53 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -624,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, diff --git a/src/basic/time-util.h b/src/basic/time-util.h index da31e29f12..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); diff --git a/src/test/test-time-util.c b/src/test/test-time-util.c index b036f6aba5..236a0ccbc4 100644 --- a/src/test/test-time-util.c +++ b/src/test/test-time-util.c @@ -663,6 +663,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; From 514fa9d39ae9935ef1e014a3dd48dd5856007df2 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 9 Sep 2025 09:52:45 +0900 Subject: [PATCH 3/3] time-util: fix issues in parse_timestamp() and optimize performance Previously, an input string ends with short timezone spec e.g. WET, was parsed by setting $TZ environment variable to the timezone. But the timezone might be different from the original local timezone, thus the result might not follow the timezone change in the original local timezone. This makes the check of the short timezone spec with tzname[] earlier, then it is not necessary to load another timezone file for e.g. WET, and provides expected time. This also make it use SAVE_TIMEZONE macro and drop use of forking process. This makes greatly improve performance when parsing string that contains timezone different from the current local timezone. Unfortunately, there is still one corner case that our test fails. When tzdata is built with rearguard enabled, then at least Africa/Windhoek timezone does not provide correct time, but time shifted 1 hour from the original. --- src/basic/time-util.c | 125 ++++++++++---------------------------- src/test/test-time-util.c | 44 +++++++------- 2 files changed, 54 insertions(+), 115 deletions(-) diff --git a/src/basic/time-util.c b/src/basic/time-util.c index fa24d79d53..fb54ba276c 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -966,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); @@ -1011,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. */ @@ -1031,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) { diff --git a/src/test/test-time-util.c b/src/test/test-time-util.c index 236a0ccbc4..7569f89ca0 100644 --- a/src/test/test-time-util.c +++ b/src/test/test-time-util.c @@ -403,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) {