2017-01-04 Assaf Gordon <assafgordon@gmail.com>
+ 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)'
{
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")))
{
{
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);
}
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.
"%+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;