diff --git a/src/basic/time-util.c b/src/basic/time-util.c index e54c829623..40d1bf1e09 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -13,6 +13,7 @@ #include "fd-util.h" #include "fileio.h" #include "fs-util.h" +#include "hexdecoct.h" #include "io-util.h" #include "log.h" #include "parse-util.h" @@ -624,16 +625,94 @@ char* format_timespan(char *buf, size_t l, usec_t t, usec_t accuracy) { return buf; } +const char* get_tzname(bool dst) { + /* musl leaves the DST timezone name unset if there is no DST, map this back to no DST */ + if (dst && isempty(tzname[1])) + dst = false; + + return empty_to_null(tzname[dst]); +} + int parse_gmtoff(const char *t, long *ret) { + int r; + assert(t); struct tm tm; const char *k = strptime(t, "%z", &tm); - if (!k || *k != '\0') + if (k && *k == '\0') { + /* Success! */ + if (ret) + *ret = tm.tm_gmtoff; + return 0; + } + + /* musl v1.2.5 does not support %z specifier in strptime(). Since + * https://github.com/kraj/musl/commit/fced99e93daeefb0192fd16304f978d4401d1d77 + * %z is supported, but it only supports strict RFC-822/ISO 8601 format, that is, 4 digits with sign + * (e.g. +0900 or -1400), but does not support extended format: 2 digits or colon separated 4 digits + * (e.g. +09 or -14:00). Let's add fallback logic to make it support the extended timezone spec. */ + + bool positive; + switch (*t) { + case '+': + positive = true; + break; + case '-': + positive = false; + break; + default: + return -EINVAL; + } + + t++; + r = undecchar(*t); + if (r < 0) + return r; + + usec_t u = r * 10 * USEC_PER_HOUR; + + t++; + r = undecchar(*t); + if (r < 0) + return r; + u += r * USEC_PER_HOUR; + + t++; + if (*t == '\0') /* 2 digits case */ + goto finalize; + + if (*t == ':') /* skip colon */ + t++; + + r = undecchar(*t); + if (r < 0) + return r; + if (r >= 6) /* refuse minutes equal to or larger than 60 */ return -EINVAL; - if (ret) - *ret = tm.tm_gmtoff; + u += r * 10 * USEC_PER_MINUTE; + + t++; + r = undecchar(*t); + if (r < 0) + return r; + + u += r * USEC_PER_MINUTE; + + t++; + if (*t != '\0') + return -EINVAL; + +finalize: + if (u > USEC_PER_DAY) /* refuse larger than one day */ + return -EINVAL; + + if (ret) { + long gmtoff = u / USEC_PER_SEC; + *ret = positive ? gmtoff : -gmtoff; + } + return 0; } @@ -810,7 +889,11 @@ static int parse_timestamp_impl( if (!k || *k != ' ') continue; +#ifdef __GLIBC__ + /* musl does not set tm_wday field and set 0 unless it is explicitly requested by %w or so. + * In the below, let's only check tm_wday field only when built with glibc. */ weekday = day->nr; +#endif t = k + 1; break; } @@ -1007,10 +1090,7 @@ int parse_timestamp(const char *t, usec_t *ret) { * not follow the timezone change in the current area. */ tzset(); for (int j = 0; j <= 1; j++) { - if (isempty(tzname[j])) - continue; - - if (!streq(tz, tzname[j])) + if (!streq_ptr(tz, get_tzname(j))) continue; /* The specified timezone matches tzname[] of the local timezone. */ diff --git a/src/basic/time-util.h b/src/basic/time-util.h index 322a2ece6b..b5530e3e92 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) +const char* get_tzname(bool dst); int parse_gmtoff(const char *t, long *ret); int parse_timestamp(const char *t, usec_t *ret); diff --git a/src/core/manager.c b/src/core/manager.c index 90a3f25aeb..b501d3f1c5 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -3208,7 +3208,7 @@ static int manager_dispatch_timezone_change( /* Read the new timezone */ tzset(); - log_debug("Timezone has been changed (now: %s).", tzname[daylight]); + log_debug("Timezone has been changed (now: %s).", get_tzname(daylight)); HASHMAP_FOREACH(u, m->units) if (UNIT_VTABLE(u)->timezone_change) diff --git a/src/shared/calendarspec.c b/src/shared/calendarspec.c index 0e12e55a97..ed8e202a2e 100644 --- a/src/shared/calendarspec.c +++ b/src/shared/calendarspec.c @@ -370,9 +370,10 @@ int calendar_spec_to_string(const CalendarSpec *c, char **ret) { tzset(); - if (!isempty(tzname[c->dst])) { + const char *z = get_tzname(c->dst); + if (z) { fputc(' ', f); - fputs(tzname[c->dst], f); + fputs(z, f); } } @@ -897,10 +898,11 @@ int calendar_spec_from_string(const char *p, CalendarSpec **ret) { /* Check if the local timezone was specified? */ for (j = 0; j <= 1; j++) { - if (isempty(tzname[j])) + const char *z = get_tzname(j); + if (!z) continue; - e = endswith_no_case(p, tzname[j]); + e = endswith_no_case(p, z); if (!e) continue; if (e == p) diff --git a/src/test/test-time-util.c b/src/test/test-time-util.c index 7bcf0ae7ee..f58be952ef 100644 --- a/src/test/test-time-util.c +++ b/src/test/test-time-util.c @@ -15,7 +15,7 @@ static void set_timezone(const char *tz) { ASSERT_OK(set_unset_env("TZ", tz, /* overwrite = */ true)); tzset(); - log_info("TZ=%s, tzname[0]=%s, tzname[1]=%s", strna(getenv("TZ")), strempty(tzname[0]), strempty(tzname[1])); + log_info("TZ=%s, tzname[0]=%s, tzname[1]=%s", strna(getenv("TZ")), strempty(get_tzname(/* dst= */ false)), strempty(get_tzname(/* dst= */ true))); } TEST(parse_sec) { @@ -400,6 +400,28 @@ static void test_format_timestamp_impl(usec_t x) { const char *xx = FORMAT_TIMESTAMP(x); ASSERT_NOT_NULL(xx); + /* Because of the timezone change, format_timestamp() may set timezone that is currently unused. + * E.g. Africa/Juba uses EAT since Sat Jan 15 10:00:00 2000 and until Sun Jan 31 20:59:59 2021, but + * now CAT/CAST is used there (see zdump for more details). In such cases, format_timestamp() may set + * the timezone used at the specified time (which happens when built with musl), but it may not match + * the timezone currently used, thus we may not parse back the timestamp. */ + + const char *space; + ASSERT_NOT_NULL(space = strrchr(xx, ' ')); + + const char *tz = space + 1; + if (!streq_ptr(tz, get_tzname(/* dst= */ false)) && + !streq_ptr(tz, get_tzname(/* dst= */ true)) && + parse_gmtoff(tz, NULL) < 0) { + + log_warning("@" USEC_FMT " → %s, timezone '%s' is currently unused, ignoring.", x, xx, tz); + + /* Verify the generated string except for the timezone part. Of course, in most cases, parsed + * time does not match with the input, hence only check if it is parsable. */ + ASSERT_OK(parse_timestamp(strndupa_safe(xx, space - xx), NULL)); + return; + } + usec_t y; ASSERT_OK(parse_timestamp(xx, &y)); const char *yy = FORMAT_TIMESTAMP(y); @@ -1109,15 +1131,15 @@ TEST(in_utc_timezone) { assert_se(setenv("TZ", "UTC", 1) >= 0); assert_se(in_utc_timezone()); - ASSERT_STREQ(tzname[0], "UTC"); - ASSERT_STREQ(tzname[1], "UTC"); + ASSERT_STREQ(get_tzname(/* dst= */ false), "UTC"); + ASSERT_STREQ(get_tzname(/* dst= */ true), "UTC"); assert_se(timezone == 0); assert_se(daylight == 0); assert_se(setenv("TZ", "Europe/Berlin", 1) >= 0); assert_se(!in_utc_timezone()); - ASSERT_STREQ(tzname[0], "CET"); - ASSERT_STREQ(tzname[1], "CEST"); + ASSERT_STREQ(get_tzname(/* dst= */ false), "CET"); + ASSERT_STREQ(get_tzname(/* dst= */ true), "CEST"); } TEST(map_clock_usec) { diff --git a/src/timedate/timedated.c b/src/timedate/timedated.c index 6e21d1a73a..9016f6c4a6 100644 --- a/src/timedate/timedated.c +++ b/src/timedate/timedated.c @@ -733,9 +733,9 @@ static int method_set_timezone(sd_bus_message *m, void *userdata, sd_bus_error * log_struct(LOG_INFO, LOG_MESSAGE_ID(SD_MESSAGE_TIMEZONE_CHANGE_STR), LOG_ITEM("TIMEZONE=%s", c->zone), - LOG_ITEM("TIMEZONE_SHORTNAME=%s", tzname[daylight]), + LOG_ITEM("TIMEZONE_SHORTNAME=%s", get_tzname(daylight)), LOG_ITEM("DAYLIGHT=%i", daylight), - LOG_MESSAGE("Changed time zone to '%s' (%s).", c->zone, tzname[daylight])); + LOG_MESSAGE("Changed time zone to '%s' (%s).", c->zone, get_tzname(daylight))); (void) sd_bus_emit_properties_changed(sd_bus_message_get_bus(m), "/org/freedesktop/timedate1", "org.freedesktop.timedate1", "Timezone",