From 30a55dd72dad2fd3a92f62dbfef4afd7ec72e540 Mon Sep 17 00:00:00 2001 From: Assaf Gordon Date: Wed, 4 Jan 2017 23:14:37 -0500 Subject: [PATCH] parse-datetime: add debug warning about DST changes Incorrect date arithmetic due to daylight saving time (DST) are a common (false) bug report in coreutils. Detect two such cases and print a warning: 1. year/month/day adjustments (performed on 'struct tm'), where 'mktime' returns a different isdst value. 2. hour/minute/seconds/ns adjustments (performed on 'time_t'), where the result of 'localtime(3)' on the value will return a different isdst value. Note: DST changes could be harmless or unnoticeable. Examples (with 'TZ=America/New_York'): Unnoticeable: result is 2016-Dec-14 $ date -d '2016-06-15 EDT + 6 months' +%b Dec Unnoticeable: result is 2016-Dec-15 11:00:00 $ date -d '2016-06-15 12:00:00 EDT + 6 months' +%F 2016-12-15 This is unexpected: $ date -d '2016-06-01 EDT + 6 months' +%F 2016-11-30 The new debug warnings will show: $ ./src/date --debug -d '2016-06-01 EDT + 6 months' +%F ... date: warning: daylight saving time changed after date adjustment ... * lib/parse-datetime.y (parse_datetime2): Detect DST changes, and print an appropriate warning message. --- ChangeLog | 28 ++++++++++++++++++++ lib/parse-datetime.y | 62 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/ChangeLog b/ChangeLog index 5753becf6f..2a302bb680 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,33 @@ 2017-01-04 Assaf Gordon + parse-datetime: add debug warning about DST changes + Incorrect date arithmetic due to daylight saving time (DST) are a + common (false) bug report in coreutils. + Detect two such cases and print a warning: + 1. year/month/day adjustments (performed on 'struct tm'), + where 'mktime' returns a different isdst value. + 2. hour/minute/seconds/ns adjustments (performed on 'time_t'), + where the result of 'localtime(3)' on the value will return a + different isdst value. + Note: DST changes could be harmless or unnoticeable. + Examples (with 'TZ=America/New_York'): + Unnoticeable: result is 2016-Dec-14 + $ date -d '2016-06-15 EDT + 6 months' +%b + Dec + Unnoticeable: result is 2016-Dec-15 11:00:00 + $ date -d '2016-06-15 12:00:00 EDT + 6 months' +%F + 2016-12-15 + This is unexpected: + $ date -d '2016-06-01 EDT + 6 months' +%F + 2016-11-30 + The new debug warnings will show: + $ ./src/date --debug -d '2016-06-01 EDT + 6 months' +%F + ... + date: warning: daylight saving time changed after date adjustment + ... + * lib/parse-datetime.y (parse_datetime2): Detect DST changes, and + print an appropriate warning message. + parse-datetime: add debug warning about date arithmetic Date arithmetic are done directly on the fields of 'struct tm', which can result in invalid dates. Normalization with 'mktime(3)' diff --git a/lib/parse-datetime.y b/lib/parse-datetime.y index 2e0a6096e4..52faafdbb1 100644 --- a/lib/parse-datetime.y +++ b/lib/parse-datetime.y @@ -1884,17 +1884,16 @@ parse_datetime2 (struct timespec *result, char const *p, { long int tz = pc.debug_default_input_timezone; const char* tz_env; - const char* tz_src; if (pc.timespec_seen) { tz = 0 ; - tz_src = _("'@timespec' - always UTC0"); + strncpy (dbg_tm, _("'@timespec' - always UTC0"), sizeof (dbg_tm)-1); } else if (pc.local_zones_seen || pc.zones_seen) { tz = pc.time_zone; - tz_src = _("parsed date/time string"); + strncpy (dbg_tm, _("parsed date/time string"), sizeof (dbg_tm)-1); } else if ((tz_env = getenv("TZ"))) { @@ -1902,28 +1901,40 @@ parse_datetime2 (struct timespec *result, char const *p, { snprintf (dbg_tm, sizeof(dbg_tm), _("TZ=\"%s\" in date string"), tz_env); - tz_src = dbg_tm; } else if (STREQ(tz_env,"UTC0")) { /* Special case: using 'date -u' simply set TZ=UTC0 */ - tz_src = _("TZ=UTC0 environment value or -u"); + strncpy (dbg_tm, _("TZ=UTC0 environment value or -u"), + sizeof (dbg_tm)-1); } else { snprintf (dbg_tm, sizeof(dbg_tm), _("TZ=\"%s\" environment value"), tz_env); - tz_src = dbg_tm; } } else { - tz_src = _("system default"); + strncpy (dbg_tm, _("system default"), sizeof (dbg_tm)-1); + } + + /* Ensure it's NUL terminated after strncpy */ + dbg_tm[sizeof (dbg_tm)-1] = '\0'; + + /* Account for DST changes if tLOCAL_ZONE was seen. + local timezone only changes DST and is relative to the + default timezone.*/ + if (pc.local_zones_seen && !pc.zones_seen && pc.local_isdst==1) + { + tz += 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, tz_src); + (int)(tz/60), abs ((int)tz)%60, dbg_tm); } @@ -2141,6 +2152,24 @@ parse_datetime2 (struct timespec *result, char const *p, debug_strfdatetime (&tm, &pc, dbg_tm, sizeof (dbg_tm))); + /* warn about crossing DST due to time adjustment. + Example: https://bugs.gnu.org/8357 + env TZ=Europe/Helsinki \ + date --debug \ + -d 'Mon Mar 28 00:36:07 2011 EEST 1 day ago' + + This case is different than DST changes due to time adjustment, + i.e. "1 day ago" vs "24 hours ago" are calculated in different + places. + + 'tm0.tm_isdst' contains the DST of the input date, + 'tm.tm_isdst' is the normalized result after calling + mktime(&tm). + */ + if ((tm0.tm_isdst!=-1) && (tm.tm_isdst != tm0.tm_isdst)) + dbg_printf (_("warning: daylight saving time changed after " \ + "date adjustment\n")); + /* warn if the user did not ask to adjust days but mday changed, or user did not ask to adjust months/days but the month changed. @@ -2249,6 +2278,23 @@ parse_datetime2 (struct timespec *result, char const *p, "%+ld minutes, %+ld seconds, %+ld ns),\n"), pc.rel.hour,pc.rel.minutes,pc.rel.seconds,pc.rel.ns); dbg_printf (_(" new time = %ld epoch-seconds\n"),t5); + + /* warn about crossing DST due to time adjustment. + Example: https://bugs.gnu.org/8357 + env TZ=Europe/Helsinki \ + date --debug \ + -d 'Mon Mar 28 00:36:07 2011 EEST 24 hours ago' + + This case is different than DST changes due to days adjustment, + i.e. "1 day ago" vs "24 hours ago" are calculated in different + places. + + '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)) + dbg_printf (_("warning: daylight saving time changed after " \ + "time adjustment\n")); } result->tv_sec = t5; -- 2.39.5