Hello Vim developers; attached is a suggested patch for correcting
a problem I noticed with time zones and Vim's strftime() function since
v8.1.1313, with an accompanying test.
I was not sure how best to do this portably, but I hope that the patch
description and the diff that corrects the problem on my Debian
GNU/Linux system at least points you in the right direction for
correcting the issue.
--
Tom Ryder <https://sanctum.geek.nz/>
--
--
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php
---
You received this message because you are subscribed to the Google Groups "vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/vim_dev/20190612121924.GB2593%40conan.
For more options, visit https://groups.google.com/d/optout.
>From e8140fc948c0240935d9eff410937acc187a55ff Mon Sep 17 00:00:00 2001
From: Tom Ryder <[email protected]>
Date: Thu, 13 Jun 2019 00:16:50 +1200
Subject: [PATCH] Call tzset(3) before each localtime_r(3)
The POSIX spec for localtime(3) and localtime_r(3) says:
> The localtime() function converts the calendar time timep to
> broken-down time representation, expressed relative to the user's
> specified timezone. The function acts as if it called tzset(3) and
> sets the external variables tzname with information about the current
> timezone, timezone with the difference between Coordinated Universal
> Time (UTC) and local standard time in seconds, and daylight to
> a nonzero value if daylight savings time rules apply during some part
> of the year. The return value points to a statically allocated struct
> which might be overwritten by subsequent calls to any of the date and
> time functions. The localtime_r() function does the same, but stores
> the data in a user-supplied struct. It need not set tzname, timezone,
> and daylight.
The last sentence of the specification is most relevant here. Patch
8.1.1313 (tag v8.1.1313, commit 63d2555) replaces calls to localtime(3)
with localtime_r(3) if available, but does not call tzset() before each
invocation.
On Debian GNU/Linux with GNU libc 2.24-11+deb9u4, this results in
a timezone "sticking" after the first tzset() instance, meaning that
changes to the TZ environment variable do not take effect in between
invocations.
As an example:
:let $TZ = "UTC" | echo $TZ.' -> '.strftime("%c")
UTC -> Wed 12 Jun 2019 04:10:29 UTC
:let $TZ = "Pacific/Auckland" | echo $TZ.' -> '.strftime("%c")
Pacific/Auckland -> Wed 12 Jun 2019 04:10:32 UTC
Note that neither the hour nor the reported timezone changes in the
stftime() output between invocations, even though the value of the TZ
environment value itself does change as expected.
This commit adds a call to tzset(3) before each call to localtime_r(3),
and corrects the above misbehavior, including checks for the
availability of both functions.
---
src/auto/configure | 2 +-
src/config.h.in | 2 ++
src/configure.ac | 2 +-
src/evalfunc.c | 5 +++--
src/memline.c | 5 +++--
src/testdir/test_functions.vim | 24 ++++++++++++++++++++++++
src/undo.c | 5 +++--
7 files changed, 37 insertions(+), 8 deletions(-)
diff --git a/src/auto/configure b/src/auto/configure
index 1f5ee2add..0d78f8a8d 100755
--- a/src/auto/configure
+++ b/src/auto/configure
@@ -12669,7 +12669,7 @@ for ac_func in fchdir fchown fchmod fsync getcwd
getpseudotty \
getpgid setpgid setsid sigaltstack sigstack sigset sigsetjmp sigaction \
sigprocmask sigvec strcasecmp strerror strftime stricmp strncasecmp \
strnicmp strpbrk strtol tgetent towlower towupper iswupper \
- usleep utime utimes mblen ftruncate unsetenv posix_openpt
+ tzset usleep utime utimes mblen ftruncate unsetenv posix_openpt
do :
as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
diff --git a/src/config.h.in b/src/config.h.in
index 23e301c90..9ffe99c76 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -216,6 +216,8 @@
#undef HAVE_TOWLOWER
#undef HAVE_TOWUPPER
#undef HAVE_ISWUPPER
+#undef HAVE_TOWUPPER
+#undef HAVE_TZSET
#undef HAVE_UNSETENV
#undef HAVE_USLEEP
#undef HAVE_UTIME
diff --git a/src/configure.ac b/src/configure.ac
index 773844a0d..1561d665b 100644
--- a/src/configure.ac
+++ b/src/configure.ac
@@ -3747,7 +3747,7 @@ AC_CHECK_FUNCS(fchdir fchown fchmod fsync getcwd
getpseudotty \
getpgid setpgid setsid sigaltstack sigstack sigset sigsetjmp sigaction \
sigprocmask sigvec strcasecmp strerror strftime stricmp strncasecmp \
strnicmp strpbrk strtol tgetent towlower towupper iswupper \
- usleep utime utimes mblen ftruncate unsetenv posix_openpt)
+ tzset usleep utime utimes mblen ftruncate unsetenv posix_openpt)
AC_FUNC_SELECT_ARGTYPES
AC_FUNC_FSEEKO
diff --git a/src/evalfunc.c b/src/evalfunc.c
index bc6056785..fcfe02e2d 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -13134,7 +13134,7 @@ f_str2nr(typval_T *argvars, typval_T *rettv)
f_strftime(typval_T *argvars, typval_T *rettv)
{
char_u result_buf[256];
-# ifdef HAVE_LOCALTIME_R
+# if defined(HAVE_LOCALTIME_R) && defined(HAVE_TZSET)
struct tm tmval;
# endif
struct tm *curtime;
@@ -13148,7 +13148,8 @@ f_strftime(typval_T *argvars, typval_T *rettv)
seconds = time(NULL);
else
seconds = (time_t)tv_get_number(&argvars[1]);
-# ifdef HAVE_LOCALTIME_R
+# if defined(HAVE_LOCALTIME_R) && defined(HAVE_TZSET)
+ tzset();
curtime = localtime_r(&seconds, &tmval);
# else
curtime = localtime(&seconds);
diff --git a/src/memline.c b/src/memline.c
index 5ae99dfd9..5ffad67e7 100644
--- a/src/memline.c
+++ b/src/memline.c
@@ -2093,12 +2093,13 @@ get_ctime(time_t thetime, int add_newline)
{
static char buf[50];
#ifdef HAVE_STRFTIME
-# ifdef HAVE_LOCALTIME_R
+# if defined(HAVE_LOCALTIME_R) && defined(HAVE_TZSET)
struct tm tmval;
# endif
struct tm *curtime;
-# ifdef HAVE_LOCALTIME_R
+# if defined(HAVE_LOCALTIME_R) && defined(HAVE_TZSET)
+ tzset();
curtime = localtime_r(&thetime, &tmval);
# else
curtime = localtime(&thetime);
diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim
index a2e4da6c7..f71717f73 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -187,6 +187,30 @@ func Test_strftime()
call assert_fails('call strftime([])', 'E730:')
call assert_fails('call strftime("%Y", [])', 'E745:')
+
+ " Check that the time changes after we change the timezone
+ " Save previous timezone value, if any
+ if exists('$TZ')
+ let tz = $TZ
+ endif
+
+ " Force EST and then UTC, save the current hour (24-hour clock) for each
+ let $TZ = 'EST' | let est = strftime('%H')
+ let $TZ = 'UTC' | let utc = strftime('%H')
+
+ " Those hours should be two bytes long, and should not be the same; if they
+ " are, a tzset(3) call may have failed somewhere
+ call assert_equal(strlen(est), 2)
+ call assert_equal(strlen(utc), 2)
+ call assert_notequal(est, utc)
+
+ " If we cached a timezone value, put it back, otherwise clear it
+ if exists('tz')
+ let $TZ = tz
+ else
+ unlet $TZ
+ endif
+
endfunc
func Test_resolve_unix()
diff --git a/src/undo.c b/src/undo.c
index cf3f866e8..c32503811 100644
--- a/src/undo.c
+++ b/src/undo.c
@@ -3111,14 +3111,15 @@ ex_undolist(exarg_T *eap UNUSED)
u_add_time(char_u *buf, size_t buflen, time_t tt)
{
#ifdef HAVE_STRFTIME
-# ifdef HAVE_LOCALTIME_R
+# if defined(HAVE_LOCALTIME_R) && defined(HAVE_TZSET)
struct tm tmval;
# endif
struct tm *curtime;
if (vim_time() - tt >= 100)
{
-# ifdef HAVE_LOCALTIME_R
+# if defined(HAVE_LOCALTIME_R) && defined(HAVE_TZSET)
+ tzset();
curtime = localtime_r(&tt, &tmval);
# else
curtime = localtime(&tt);
--
2.21.0