Skip to content

Commit 6d8e9f9

Browse files
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.
1 parent b86ff49 commit 6d8e9f9

File tree

2 files changed

+58
-10
lines changed

2 files changed

+58
-10
lines changed

src/builtin.c

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,7 +1594,7 @@ static jv f_strptime(jq_state *jq, jv a, jv b) {
15941594
return r;
15951595
}
15961596

1597-
static int jv2tm(jv a, struct tm *tm) {
1597+
static int jv2tm(jv a, struct tm *tm, int localtime) {
15981598
memset(tm, 0, sizeof(*tm));
15991599
static const size_t offsets[] = {
16001600
offsetof(struct tm, tm_year),
@@ -1624,13 +1624,25 @@ static int jv2tm(jv a, struct tm *tm) {
16241624
jv_free(n);
16251625
}
16261626

1627-
// We use UTC everywhere (gettimeofday, gmtime) and UTC does not do DST.
1628-
// Setting tm_isdst to 0 is done by the memset.
1629-
// tm->tm_isdst = 0;
1627+
if (localtime) {
1628+
tm->tm_isdst = -1;
1629+
mktime(tm);
1630+
} else {
1631+
#ifdef HAVE_TIMEGM
1632+
timegm(tm);
1633+
#elif HAVE_TM_TM_GMT_OFF
1634+
// tm->tm_gmtoff = 0;
1635+
tm->tm_zone = "GMT";
1636+
#elif HAVE_TM___TM_GMT_OFF
1637+
// tm->__tm_gmtoff = 0;
1638+
tm->__tm_zone = "GMT";
1639+
#endif
1640+
// tm->tm_isdst = 0;
16301641

1631-
// The standard permits the tm structure to contain additional members. We
1632-
// hope it is okay to initialize them to zero, because the standard does not
1633-
// provide an alternative.
1642+
// The standard permits the tm structure to contain additional members. We
1643+
// hope it is okay to initialize them to zero, because the standard does not
1644+
// provide an alternative.
1645+
}
16341646

16351647
jv_free(a);
16361648
return 1;
@@ -1642,7 +1654,7 @@ static jv f_mktime(jq_state *jq, jv a) {
16421654
if (jv_get_kind(a) != JV_KIND_ARRAY)
16431655
return ret_error(a, jv_string("mktime requires array inputs"));
16441656
struct tm tm;
1645-
if (!jv2tm(a, &tm))
1657+
if (!jv2tm(a, &tm, 0))
16461658
return jv_invalid_with_msg(jv_string("mktime requires parsed datetime inputs"));
16471659
time_t t = my_mktime(&tm);
16481660
if (t == (time_t)-1)
@@ -1740,13 +1752,27 @@ static jv f_strftime(jq_state *jq, jv a, jv b) {
17401752
if (jv_get_kind(b) != JV_KIND_STRING)
17411753
return ret_error2(a, b, jv_string("strftime/1 requires a string format"));
17421754
struct tm tm;
1743-
if (!jv2tm(a, &tm))
1755+
if (!jv2tm(a, &tm, 0))
17441756
return ret_error(b, jv_string("strftime/1 requires parsed datetime inputs"));
17451757

17461758
const char *fmt = jv_string_value(b);
17471759
size_t alloced = strlen(fmt) + 100;
17481760
char *buf = alloca(alloced);
1761+
#ifdef __APPLE__
1762+
/* Apple Libc (as of version 1669.40.2) contains a bug which causes it to
1763+
* ignore the `tm.tm_gmtoff` in favor of the global timezone. To print the
1764+
* proper timezone offset we temporarily switch the TZ to UTC. */
1765+
char *tz = getenv("TZ");
1766+
setenv("TZ", "UTC", 1);
1767+
#endif
17491768
size_t n = strftime(buf, alloced, fmt, &tm);
1769+
#ifdef __APPLE__
1770+
if (tz) {
1771+
setenv("TZ", tz, 1);
1772+
} else {
1773+
unsetenv("TZ");
1774+
}
1775+
#endif
17501776
jv_free(b);
17511777
/* POSIX doesn't provide errno values for strftime() failures; weird */
17521778
if (n == 0 || n > alloced)
@@ -1771,7 +1797,7 @@ static jv f_strflocaltime(jq_state *jq, jv a, jv b) {
17711797
if (jv_get_kind(b) != JV_KIND_STRING)
17721798
return ret_error2(a, b, jv_string("strflocaltime/1 requires a string format"));
17731799
struct tm tm;
1774-
if (!jv2tm(a, &tm))
1800+
if (!jv2tm(a, &tm, 1))
17751801
return ret_error(b, jv_string("strflocaltime/1 requires parsed datetime inputs"));
17761802
const char *fmt = jv_string_value(b);
17771803
size_t alloced = strlen(fmt) + 100;

tests/shtest

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,4 +694,26 @@ printf '[\n {\n "a": 1\n }\n]\n' > $d/expected
694694
$JQ --indent 6 -n "[{a:1}]" > $d/out
695695
cmp $d/out $d/expected
696696

697+
if ! $msys && ! $mingw; then
698+
# Test handling of timezones -- #2429, #2475
699+
if ! r=$(TZ=Asia/Tokyo $JQ -rn '1731627341 | strflocaltime("%F %T %z %Z")') \
700+
|| [ "$r" != "2024-11-15 08:35:41 +0900 JST" ]; then
701+
echo "Incorrectly formatted local time"
702+
exit 1
703+
fi
704+
705+
if ! r=$(TZ=Europe/Paris $JQ -rn '1731627341 | strflocaltime("%F %T %z %Z")') \
706+
|| [ "$r" != "2024-11-15 00:35:41 +0100 CET" ]; then
707+
echo "Incorrectly formatted local time"
708+
exit 1
709+
fi
710+
711+
if ! r=$(TZ=Europe/Paris $JQ -rn '1731627341 | strftime("%F %T %z %Z")') \
712+
|| ( [ "$r" != "2024-11-14 23:35:41 +0000 UTC" ] \
713+
&& [ "$r" != "2024-11-14 23:35:41 +0000 GMT" ] ); then
714+
echo "Incorrectly formatted universal time"
715+
exit 1
716+
fi
717+
fi
718+
697719
exit 0

0 commit comments

Comments
 (0)