From 4e6e16b3f43ce96302b1e52e48730c1f15e18c14 Mon Sep 17 00:00:00 2001 From: Paul Eggert Date: Fri, 20 Jan 2017 17:11:55 -0800 Subject: [PATCH] parse-datetime: handle timezones reentrantly MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This API change was prompted by a report by Pádraig Brady in: https://bug.debian.org/851934#10 To help fix the bug, make parse_datetime2 more reentrant. * NEWS: Document this incompatible change. * lib/parse-datetime.h, lib/parse-datetime.y (parse_datetime2): Add two arguments, the timezone and the timezone name. All callers changed. If TZ="..." is specified, use it for calculating defaults. * lib/parse-datetime.y: Don't include xalloc.h or use xmalloc, as this code should be usable in a library. (mktime_ok, get_effective_timezone): Accept timezone arg too. All callers changed. (get_tz): Remove. (get_effective_timezone): Check for failures. * modules/parse-datetime: Add time_r, time_rz. Remove xalloc. --- ChangeLog | 18 ++++ NEWS | 4 + lib/parse-datetime.h | 2 +- lib/parse-datetime.y | 228 ++++++++++++++++++++--------------------- modules/parse-datetime | 3 +- 5 files changed, 135 insertions(+), 120 deletions(-) diff --git a/ChangeLog b/ChangeLog index 41fbf4a31d..29d5954f8f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,21 @@ +2017-01-20 Paul Eggert + + parse-datetime: handle timezones reentrantly + This API change was prompted by a report by Pádraig Brady in: + https://bug.debian.org/851934#10 + To help fix the bug, make parse_datetime2 more reentrant. + * NEWS: Document this incompatible change. + * lib/parse-datetime.h, lib/parse-datetime.y (parse_datetime2): + Add two arguments, the timezone and the timezone name. + All callers changed. If TZ="..." is specified, use it for + calculating defaults. + * lib/parse-datetime.y: Don't include xalloc.h or use xmalloc, as + this code should be usable in a library. + (mktime_ok, get_effective_timezone): + Accept timezone arg too. All callers changed. + (get_tz): Remove. + (get_effective_timezone): Check for failures. + 2017-01-20 Eric Blake localename: port to cygwin 2.6 diff --git a/NEWS b/NEWS index 90ce1a2ccc..266275a945 100644 --- a/NEWS +++ b/NEWS @@ -42,6 +42,10 @@ User visible incompatible changes Date Modules Changes +2017-01-20 parse-datetime The parse_datetime2 function now takes two + more arguments TZ and TZSTRING, for the + time zone and its name. + 2017-01-16 host-cpu-c-abi On ARM platforms, HOST_CPU_C_ABI is now set to 'arm' or 'armhf' instead of 'armel'. diff --git a/lib/parse-datetime.h b/lib/parse-datetime.h index fa6a9a3769..2be5faa837 100644 --- a/lib/parse-datetime.h +++ b/lib/parse-datetime.h @@ -26,4 +26,4 @@ bool parse_datetime (struct timespec *, char const *, struct timespec const *); /* same as above, supporting additional flags */ bool parse_datetime2 (struct timespec *, char const *, struct timespec const *, - unsigned int flags); + unsigned int flags, timezone_t, char const *); diff --git a/lib/parse-datetime.y b/lib/parse-datetime.y index c604be6f77..6af4f2945b 100644 --- a/lib/parse-datetime.y +++ b/lib/parse-datetime.y @@ -68,7 +68,6 @@ #include #include "gettext.h" -#include "xalloc.h" #define _(str) gettext (str) @@ -1531,19 +1530,21 @@ yyerror (parser_control const *pc _GL_UNUSED, return 0; } -/* If *TM0 is the old and *TM1 is the new value of a struct tm after - passing it to mktime, return true if it's OK that mktime returned T. - It's not OK if *TM0 has out-of-range members. */ +/* In timezone TZ, if *TM0 is the old and *TM1 is the new value of a + struct tm after passing it to mktime_z, return true if it's OK that + mktime_z returned T. It's not OK if *TM0 has out-of-range + members. */ static bool -mktime_ok (struct tm const *tm0, struct tm const *tm1, time_t t) +mktime_ok (timezone_t tz, struct tm const *tm0, struct tm const *tm1, time_t t) { + struct tm ltm; if (t == (time_t) -1) { /* Guard against falsely reporting an error when parsing a timestamp that happens to equal (time_t) -1, on a host that supports such a timestamp. */ - tm1 = localtime (&t); + tm1 = localtime_rz (tz, &t, <m); if (!tm1) return false; } @@ -1564,22 +1565,6 @@ enum { TZBUFSIZE = 100 }; see days_to_name(), debug_strftime() and debug_mktime_not_ok() */ enum { DBGBUFSIZE = 100 }; -/* Return a copy of TZ, stored in TZBUF if it fits, and heap-allocated - otherwise. */ -static char * -get_tz (char tzbuf[TZBUFSIZE]) -{ - char *tz = getenv ("TZ"); - if (tz) - { - size_t tzsize = strlen (tz) + 1; - tz = (tzsize <= TZBUFSIZE - ? memcpy (tzbuf, tz, tzsize) - : xmemdup (tz, tzsize)); - } - return tz; -} - /* debugging: format a 'struct tm' into a buffer, taking the parser's timezone information into account (if pc!=NULL). */ static const char* @@ -1708,48 +1693,54 @@ debug_mktime_not_ok (struct tm const *tm0, struct tm const *tm1, /* Returns the effective local timezone, in minutes. */ static long int -get_effective_timezone (void) +get_effective_timezone (timezone_t tz) { - /* TODO: check for failures */ - const time_t z = 0; - time_t lz ; - struct tm *ltm; - ltm = localtime (&z); - lz = timegm (ltm)/60; - return (long int)lz; + time_t z = 0; + struct tm tm; + if (! localtime_rz (tz, &z, &tm)) + return 0; + return timegm (&tm) / 60; } -/* The original interface: run with debug=false */ +/* The original interface: run with debug=false and the default timezone. */ bool parse_datetime (struct timespec *result, char const *p, struct timespec const *now) { - return parse_datetime2 (result, p, now, 0); + char const *tzstring = getenv ("TZ"); + timezone_t tz = tzalloc (tzstring); + if (!tz) + return false; + bool ok = parse_datetime2 (result, p, now, 0, tz, tzstring); + tzfree (tz); + return ok; } /* Parse a date/time string, storing the resulting time value into *RESULT. The string itself is pointed to by P. Return true if successful. P can be an incomplete or relative time specification; if so, use - *NOW as the basis for the returned time. */ + *NOW as the basis for the returned time. Default to timezone + TZDEFAULT, which corresponds to tzalloc (TZSTRING). */ bool parse_datetime2 (struct timespec *result, char const *p, - struct timespec const *now, unsigned int flags) + struct timespec const *now, unsigned int flags, + timezone_t tzdefault, char const *tzstring) { time_t Start; long int Start_ns; - struct tm const *tmp; + struct tm tmp; struct tm tm; struct tm tm0; parser_control pc; struct timespec gettime_buffer; unsigned char c; - bool tz_was_altered = false; - char *tz0 = NULL; - char tz0buf[TZBUFSIZE]; + timezone_t tz = tzdefault; bool ok = true; char dbg_ord[DBGBUFSIZE]; char dbg_tm[DBGBUFSIZE]; char const *input_sentinel = p + strlen (p); + char *tz1alloc = NULL; + char tz1buf[TZBUFSIZE]; if (! now) { @@ -1760,10 +1751,6 @@ parse_datetime2 (struct timespec *result, char const *p, Start = now->tv_sec; Start_ns = now->tv_nsec; - tmp = localtime (&now->tv_sec); - if (! tmp) - return false; - while (c = *p, c_isspace (c)) p++; @@ -1782,22 +1769,25 @@ parse_datetime2 (struct timespec *result, char const *p, } else if (*s == '"') { + timezone_t tz1; + char *tz1string = tz1buf; char *z; - char *tz1; - char tz1buf[TZBUFSIZE]; - bool large_tz = TZBUFSIZE < tzsize; - bool setenv_ok; - tz0 = get_tz (tz0buf); - z = tz1 = large_tz ? xmalloc (tzsize) : tz1buf; + if (TZBUFSIZE < tzsize) + { + tz1alloc = malloc (tzsize); + if (!tz1alloc) + goto fail; + tz1string = tz1alloc; + } + z = tz1string; for (s = tzbase; *s != '"'; s++) *z++ = *(s += *s == '\\'); *z = '\0'; - setenv_ok = setenv ("TZ", tz1, 1) == 0; - if (large_tz) - free (tz1); - if (!setenv_ok) + tz1 = tzalloc (tz1string); + if (!tz1) goto fail; - tz_was_altered = true; + tz = tz1; + tzstring = tz1string; p = s + 1; while (c = *p, c_isspace (c)) @@ -1807,6 +1797,9 @@ parse_datetime2 (struct timespec *result, char const *p, } } + if (! localtime_rz (tz, &now->tv_sec, &tmp)) + return false; + /* As documented, be careful to treat the empty string just like a date string of "0". Without this, an empty string would be declared invalid when parsed during a DST transition. */ @@ -1814,16 +1807,16 @@ parse_datetime2 (struct timespec *result, char const *p, p = "0"; pc.input = p; - pc.year.value = tmp->tm_year; + pc.year.value = tmp.tm_year; pc.year.value += TM_YEAR_BASE; pc.year.digits = 0; - pc.month = tmp->tm_mon + 1; - pc.day = tmp->tm_mday; - pc.hour = tmp->tm_hour; - pc.minutes = tmp->tm_min; - pc.seconds.tv_sec = tmp->tm_sec; + pc.month = tmp.tm_mon + 1; + pc.day = tmp.tm_mday; + pc.hour = tmp.tm_hour; + pc.minutes = tmp.tm_min; + pc.seconds.tv_sec = tmp.tm_sec; pc.seconds.tv_nsec = Start_ns; - tm.tm_isdst = tmp->tm_isdst; + tm.tm_isdst = tmp.tm_isdst; pc.meridian = MER24; pc.rel = RELATIVE_TIME_0; @@ -1848,9 +1841,9 @@ parse_datetime2 (struct timespec *result, char const *p, pc.debug_default_input_timezone = 0; #if HAVE_STRUCT_TM_TM_ZONE - pc.local_time_zone_table[0].name = tmp->tm_zone; + pc.local_time_zone_table[0].name = tmp.tm_zone; pc.local_time_zone_table[0].type = tLOCAL_ZONE; - pc.local_time_zone_table[0].value = tmp->tm_isdst; + pc.local_time_zone_table[0].value = tmp.tm_isdst; pc.local_time_zone_table[1].name = NULL; /* Probe the names used in the next three calendar quarters, looking @@ -1860,14 +1853,14 @@ parse_datetime2 (struct timespec *result, char const *p, for (quarter = 1; quarter <= 3; quarter++) { time_t probe = Start + quarter * (90 * 24 * 60 * 60); - struct tm const *probe_tm = localtime (&probe); - if (probe_tm && probe_tm->tm_zone - && probe_tm->tm_isdst != pc.local_time_zone_table[0].value) + struct tm probe_tm; + if (localtime_rz (tz, &probe, &probe_tm) && probe_tm.tm_zone + && probe_tm.tm_isdst != pc.local_time_zone_table[0].value) { { - pc.local_time_zone_table[1].name = probe_tm->tm_zone; + pc.local_time_zone_table[1].name = probe_tm.tm_zone; pc.local_time_zone_table[1].type = tLOCAL_ZONE; - pc.local_time_zone_table[1].value = probe_tm->tm_isdst; + pc.local_time_zone_table[1].value = probe_tm.tm_isdst; pc.local_time_zone_table[2].name = NULL; } break; @@ -1905,7 +1898,7 @@ parse_datetime2 (struct timespec *result, char const *p, pc.local_time_zone_table[1].name = NULL; } - pc.debug_default_input_timezone = get_effective_timezone (); + pc.debug_default_input_timezone = get_effective_timezone (tz); if (yyparse (&pc) != 0) { @@ -1925,27 +1918,26 @@ parse_datetime2 (struct timespec *result, char const *p, /* determine effective timezone source */ if (pc.parse_datetime_debug) { - long int tz = pc.debug_default_input_timezone; - const char* tz_env; + long int time_zone = pc.debug_default_input_timezone; if (pc.timespec_seen) { - tz = 0 ; + time_zone = 0; strncpy (dbg_tm, _("'@timespec' - always UTC0"), sizeof (dbg_tm)-1); } else if (pc.zones_seen) { - tz = pc.time_zone; + time_zone = pc.time_zone; strncpy (dbg_tm, _("parsed date/time string"), sizeof (dbg_tm)-1); } - else if ((tz_env = getenv("TZ"))) + else if (tzstring) { - if (tz_was_altered) + if (tz != tzdefault) { snprintf (dbg_tm, sizeof(dbg_tm), _("TZ=\"%s\" in date string"), - tz_env); + tzstring); } - else if (STREQ(tz_env,"UTC0")) + else if (STREQ (tzstring, "UTC0")) { /* Special case: using 'date -u' simply set TZ=UTC0 */ strncpy (dbg_tm, _("TZ=UTC0 environment value or -u"), @@ -1954,7 +1946,7 @@ parse_datetime2 (struct timespec *result, char const *p, else { snprintf (dbg_tm, sizeof(dbg_tm), - _("TZ=\"%s\" environment value"), tz_env); + _("TZ=\"%s\" environment value"), tzstring); } } else @@ -1970,14 +1962,15 @@ parse_datetime2 (struct timespec *result, char const *p, default timezone.*/ if (pc.local_zones_seen && !pc.zones_seen && pc.local_isdst==1) { - tz += 60; + time_zone += 60; strncat (dbg_tm, ", dst", sizeof (dbg_tm) - strlen (dbg_tm) - 1); } if (pc.parse_datetime_debug) dbg_printf (_("input timezone: %+03d:%02d (set from %s)\n"), - (int)(tz/60), abs ((int)tz)%60, dbg_tm); + (int) (time_zone / 60), abs ((int) (time_zone % 60)), + dbg_tm); } @@ -2045,9 +2038,9 @@ parse_datetime2 (struct timespec *result, char const *p, tm0 = tm; - Start = mktime (&tm); + Start = mktime_z (tz, &tm); - if (! mktime_ok (&tm0, &tm, Start)) + if (! mktime_ok (tz, &tm0, &tm, Start)) { if (! pc.zones_seen) { @@ -2071,27 +2064,27 @@ parse_datetime2 (struct timespec *result, char const *p, long int abs_time_zone = time_zone < 0 ? - time_zone : time_zone; long int abs_time_zone_hour = abs_time_zone / 60; int abs_time_zone_min = abs_time_zone % 60; - char tz1buf[sizeof "XXX+0:00" + TYPE_WIDTH (pc.time_zone) / 3]; - if (!tz_was_altered) - tz0 = get_tz (tz0buf); - sprintf (tz1buf, "XXX%s%ld:%02d", &"-"[time_zone < 0], + char tz2buf[sizeof "XXX+0:00" + TYPE_WIDTH (pc.time_zone) / 3]; + timezone_t tz2; + sprintf (tz2buf, "XXX%s%ld:%02d", &"-"[time_zone < 0], abs_time_zone_hour, abs_time_zone_min); - if (setenv ("TZ", tz1buf, 1) != 0) + tz2 = tzalloc (tz2buf); + if (!tz2) { - /* TODO: was warn () + print errno? */ if (pc.parse_datetime_debug) - dbg_printf (_("error: setenv('TZ','%s') failed\n"), tz1buf); + dbg_printf (_("error: tzalloc (\"%s\") failed\n"), tz2buf); goto fail; } - tz_was_altered = true; tm = tm0; - Start = mktime (&tm); - if (! mktime_ok (&tm0, &tm, Start)) + Start = mktime_z (tz2, &tm); + ok = mktime_ok (tz2, &tm0, &tm, Start); + tzfree (tz2); + if (! ok) { debug_mktime_not_ok (&tm0, &tm, &pc, pc.zones_seen); - goto fail; + goto done; } } } @@ -2103,7 +2096,7 @@ parse_datetime2 (struct timespec *result, char const *p, - (0 < pc.day_ordinal && tm.tm_wday != pc.day_number))); tm.tm_isdst = -1; - Start = mktime (&tm); + Start = mktime_z (tz, &tm); if (Start == (time_t) -1) { if (pc.parse_datetime_debug) @@ -2174,7 +2167,7 @@ parse_datetime2 (struct timespec *result, char const *p, tm.tm_min = tm0.tm_min; tm.tm_sec = tm0.tm_sec; tm.tm_isdst = tm0.tm_isdst; - Start = mktime (&tm); + Start = mktime_z (tz, &tm); if (Start == (time_t) -1) { if (pc.parse_datetime_debug) @@ -2250,8 +2243,8 @@ parse_datetime2 (struct timespec *result, char const *p, delta -= tm.tm_gmtoff; #else time_t t = Start; - struct tm const *gmt = gmtime (&t); - if (! gmt) + struct tm gmt; + if (! gmtime_r (&t, &gmt)) { /* TODO: use 'warn(3)' + print errno ? */ if (pc.parse_datetime_debug) @@ -2259,7 +2252,7 @@ parse_datetime2 (struct timespec *result, char const *p, goto fail; } - delta -= tm_diff (&tm, gmt); + delta -= tm_diff (&tm, &gmt); #endif t1 = Start - delta; if ((Start < t1) != (delta < 0)) @@ -2317,6 +2310,7 @@ parse_datetime2 (struct timespec *result, char const *p, if (pc.parse_datetime_debug && (pc.rel.hour | pc.rel.minutes | pc.rel.seconds | pc.rel.ns)) { + struct tm lmt; dbg_printf (_("after time adjustment (%+ld hours, " \ "%+ld minutes, %+ld seconds, %+ld ns),\n"), pc.rel.hour,pc.rel.minutes,pc.rel.seconds,pc.rel.ns); @@ -2334,8 +2328,8 @@ parse_datetime2 (struct timespec *result, char const *p, 'tm.tm_isdst' contains the date after date adjustment. */ - struct tm const *lmt = localtime (&t5); - if ((tm.tm_isdst!=-1) && (tm.tm_isdst != lmt->tm_isdst)) + if (tm.tm_isdst != -1 && localtime_rz (tz, &t5, &lmt) + && tm.tm_isdst != lmt.tm_isdst) dbg_printf (_("warning: daylight saving time changed after " \ "time adjustment\n")); } @@ -2350,29 +2344,22 @@ parse_datetime2 (struct timespec *result, char const *p, fail: ok = false; done: - if (tz_was_altered) - ok &= (tz0 ? setenv ("TZ", tz0, 1) : unsetenv ("TZ")) == 0; - if (tz0 != tz0buf) - free (tz0); - if (ok && pc.parse_datetime_debug) { - /* print local timezone AFTER restoring TZ (if tz_was_altered)*/ - const long int otz = get_effective_timezone (); - const char* tz_src; - const char* tz_env; + const long int otz = get_effective_timezone (tz); + const char *tz_src; - if ((tz_env = getenv("TZ"))) + if (tzstring) { /* Special case: using 'date -u' simply set TZ=UTC0 */ - if (STREQ(tz_env,"UTC0")) + if (STREQ (tzstring, "UTC0")) { tz_src = _("TZ=UTC0 environment value or -u"); } else { snprintf (dbg_tm, sizeof(dbg_tm), - _("TZ=\"%s\" environment value"), tz_env); + _("TZ=\"%s\" environment value"), tzstring); tz_src = dbg_tm; } } @@ -2390,16 +2377,21 @@ parse_datetime2 (struct timespec *result, char const *p, dbg_printf (_("final: %ld.%09ld (epoch-seconds)\n"), result->tv_sec,result->tv_nsec); - struct tm const *gmt = gmtime (&result->tv_sec); - dbg_printf (_("final: %s (UTC0)\n"), - debug_strfdatetime (gmt, NULL, dbg_tm, sizeof (dbg_tm))); - struct tm const *lmt = localtime (&result->tv_sec); - dbg_printf (_("final: %s (output timezone TZ=%+03d:%02d)\n"), - debug_strfdatetime (lmt, NULL, dbg_tm, sizeof (dbg_tm)), - (int)(otz/60), abs ((int)otz)%60); + struct tm gmt, lmt; + if (gmtime_r (&result->tv_sec, &gmt)) + dbg_printf (_("final: %s (UTC0)\n"), + debug_strfdatetime (&gmt, NULL, + dbg_tm, sizeof dbg_tm)); + if (localtime_rz (tz, &result->tv_sec, &lmt)) + dbg_printf (_("final: %s (output timezone TZ=%+03d:%02d)\n"), + debug_strfdatetime (&lmt, NULL, dbg_tm, sizeof dbg_tm), + (int) (otz / 60), abs ((int) (otz % 60))); } } + if (tz != tzdefault) + tzfree (tz); + free (tz1alloc); return ok; } diff --git a/modules/parse-datetime b/modules/parse-datetime index 20d4236417..5d10137425 100644 --- a/modules/parse-datetime +++ b/modules/parse-datetime @@ -20,9 +20,10 @@ setenv strftime unsetenv time +time_r +time_rz timegm verify -xalloc configure.ac: gl_PARSE_DATETIME -- 2.39.5