time-util: several fixlets/workarounds for supporting musl (#39688)

This commit is contained in:
Yu Watanabe
2025-11-13 05:18:25 +09:00
committed by GitHub
6 changed files with 124 additions and 19 deletions

View File

@@ -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. */

View File

@@ -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);

View File

@@ -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)

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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",