fix: populate timezone data when formatting time (#3203)
The `jv2tm` function was zeroing fields of `struct tm` that were not specified by the standard. However, depending on the libc this produced incorrect timezone data when used together with formatting functions. This change tries to fill the timezone data using either `mktime`, `timegm`, or manually. Apple's Libc implementation contains a bug which causes it to ignore the offset data present in the `struct tm` in favor of the older heuristic needed by legacy standards. This workaround temporarily sets the global timezone so it gets picked up during formatting.
This commit is contained in:
1 parent
b86ff49f46
commit
6d8e9f98ae
2 files changed
+58
-10
No files matched your search
+36
-10
@@ -1594,7 +1594,7 @@ static jv f_strptime(jq_state *jq, jv a, jv b) {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int jv2tm(jv a, struct tm *tm) {
|
static int jv2tm(jv a, struct tm *tm, int localtime) {
|
||||||
memset(tm, 0, sizeof(*tm));
|
memset(tm, 0, sizeof(*tm));
|
||||||
static const size_t offsets[] = {
|
static const size_t offsets[] = {
|
||||||
offsetof(struct tm, tm_year),
|
offsetof(struct tm, tm_year),
|
||||||
@@ -1624,13 +1624,25 @@ static int jv2tm(jv a, struct tm *tm) {
|
|||||||
jv_free(n);
|
jv_free(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use UTC everywhere (gettimeofday, gmtime) and UTC does not do DST.
|
if (localtime) {
|
||||||
// Setting tm_isdst to 0 is done by the memset.
|
tm->tm_isdst = -1;
|
||||||
// tm->tm_isdst = 0;
|
mktime(tm);
|
||||||
|
} else {
|
||||||
|
#ifdef HAVE_TIMEGM
|
||||||
|
timegm(tm);
|
||||||
|
#elif HAVE_TM_TM_GMT_OFF
|
||||||
|
// tm->tm_gmtoff = 0;
|
||||||
|
tm->tm_zone = "GMT";
|
||||||
|
#elif HAVE_TM___TM_GMT_OFF
|
||||||
|
// tm->__tm_gmtoff = 0;
|
||||||
|
tm->__tm_zone = "GMT";
|
||||||
|
#endif
|
||||||
|
// tm->tm_isdst = 0;
|
||||||
|
|
||||||
// The standard permits the tm structure to contain additional members. We
|
// The standard permits the tm structure to contain additional members. We
|
||||||
// hope it is okay to initialize them to zero, because the standard does not
|
// hope it is okay to initialize them to zero, because the standard does not
|
||||||
// provide an alternative.
|
// provide an alternative.
|
||||||
|
}
|
||||||
|
|
||||||
jv_free(a);
|
jv_free(a);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1642,7 +1654,7 @@ static jv f_mktime(jq_state *jq, jv a) {
|
|||||||
if (jv_get_kind(a) != JV_KIND_ARRAY)
|
if (jv_get_kind(a) != JV_KIND_ARRAY)
|
||||||
return ret_error(a, jv_string("mktime requires array inputs"));
|
return ret_error(a, jv_string("mktime requires array inputs"));
|
||||||
struct tm tm;
|
struct tm tm;
|
||||||
if (!jv2tm(a, &tm))
|
if (!jv2tm(a, &tm, 0))
|
||||||
return jv_invalid_with_msg(jv_string("mktime requires parsed datetime inputs"));
|
return jv_invalid_with_msg(jv_string("mktime requires parsed datetime inputs"));
|
||||||
time_t t = my_mktime(&tm);
|
time_t t = my_mktime(&tm);
|
||||||
if (t == (time_t)-1)
|
if (t == (time_t)-1)
|
||||||
@@ -1740,13 +1752,27 @@ static jv f_strftime(jq_state *jq, jv a, jv b) {
|
|||||||
if (jv_get_kind(b) != JV_KIND_STRING)
|
if (jv_get_kind(b) != JV_KIND_STRING)
|
||||||
return ret_error2(a, b, jv_string("strftime/1 requires a string format"));
|
return ret_error2(a, b, jv_string("strftime/1 requires a string format"));
|
||||||
struct tm tm;
|
struct tm tm;
|
||||||
if (!jv2tm(a, &tm))
|
if (!jv2tm(a, &tm, 0))
|
||||||
return ret_error(b, jv_string("strftime/1 requires parsed datetime inputs"));
|
return ret_error(b, jv_string("strftime/1 requires parsed datetime inputs"));
|
||||||
|
|
||||||
const char *fmt = jv_string_value(b);
|
const char *fmt = jv_string_value(b);
|
||||||
size_t alloced = strlen(fmt) + 100;
|
size_t alloced = strlen(fmt) + 100;
|
||||||
char *buf = alloca(alloced);
|
char *buf = alloca(alloced);
|
||||||
|
#ifdef __APPLE__
|
||||||
|
/* Apple Libc (as of version 1669.40.2) contains a bug which causes it to
|
||||||
|
* ignore the `tm.tm_gmtoff` in favor of the global timezone. To print the
|
||||||
|
* proper timezone offset we temporarily switch the TZ to UTC. */
|
||||||
|
char *tz = getenv("TZ");
|
||||||
|
setenv("TZ", "UTC", 1);
|
||||||
|
#endif
|
||||||
size_t n = strftime(buf, alloced, fmt, &tm);
|
size_t n = strftime(buf, alloced, fmt, &tm);
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (tz) {
|
||||||
|
setenv("TZ", tz, 1);
|
||||||
|
} else {
|
||||||
|
unsetenv("TZ");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
jv_free(b);
|
jv_free(b);
|
||||||
/* POSIX doesn't provide errno values for strftime() failures; weird */
|
/* POSIX doesn't provide errno values for strftime() failures; weird */
|
||||||
if (n == 0 || n > alloced)
|
if (n == 0 || n > alloced)
|
||||||
@@ -1771,7 +1797,7 @@ static jv f_strflocaltime(jq_state *jq, jv a, jv b) {
|
|||||||
if (jv_get_kind(b) != JV_KIND_STRING)
|
if (jv_get_kind(b) != JV_KIND_STRING)
|
||||||
return ret_error2(a, b, jv_string("strflocaltime/1 requires a string format"));
|
return ret_error2(a, b, jv_string("strflocaltime/1 requires a string format"));
|
||||||
struct tm tm;
|
struct tm tm;
|
||||||
if (!jv2tm(a, &tm))
|
if (!jv2tm(a, &tm, 1))
|
||||||
return ret_error(b, jv_string("strflocaltime/1 requires parsed datetime inputs"));
|
return ret_error(b, jv_string("strflocaltime/1 requires parsed datetime inputs"));
|
||||||
const char *fmt = jv_string_value(b);
|
const char *fmt = jv_string_value(b);
|
||||||
size_t alloced = strlen(fmt) + 100;
|
size_t alloced = strlen(fmt) + 100;
|
||||||
|
|||||||
@@ -694,4 +694,26 @@ printf '[\n {\n "a": 1\n }\n]\n' > $d/expected
|
|||||||
$JQ --indent 6 -n "[{a:1}]" > $d/out
|
$JQ --indent 6 -n "[{a:1}]" > $d/out
|
||||||
cmp $d/out $d/expected
|
cmp $d/out $d/expected
|
||||||
|
|
||||||
|
if ! $msys && ! $mingw; then
|
||||||
|
# Test handling of timezones -- #2429, #2475
|
||||||
|
if ! r=$(TZ=Asia/Tokyo $JQ -rn '1731627341 | strflocaltime("%F %T %z %Z")') \
|
||||||
|
|| [ "$r" != "2024-11-15 08:35:41 +0900 JST" ]; then
|
||||||
|
echo "Incorrectly formatted local time"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! r=$(TZ=Europe/Paris $JQ -rn '1731627341 | strflocaltime("%F %T %z %Z")') \
|
||||||
|
|| [ "$r" != "2024-11-15 00:35:41 +0100 CET" ]; then
|
||||||
|
echo "Incorrectly formatted local time"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! r=$(TZ=Europe/Paris $JQ -rn '1731627341 | strftime("%F %T %z %Z")') \
|
||||||
|
|| ( [ "$r" != "2024-11-14 23:35:41 +0000 UTC" ] \
|
||||||
|
&& [ "$r" != "2024-11-14 23:35:41 +0000 GMT" ] ); then
|
||||||
|
echo "Incorrectly formatted universal time"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
Reference in new issue
Block a user