Subject: Re: bug#48085: date -d greater than 23 years ago gives error invalid date

From: Paul Eggert <[email-masked]>
Date: Sat, 19 Feb 2022 15:57:16 -0800

On 4/28/21 16:23, Mark Krenz wrote:
> So I'm not sure if this is a problem with coreutils or a change in the
> zoneinfo database. Any ideas?

This appears to be a problem in the GNU C library, when its mktime 
deciphers the relatively unusual time zone history of Indiana.

I installed the attached patch into Gnulib and propagated it into 
Coreutils, so the issue should be fixed in the next release of GNU 
Coreutils. Eventually this patch should migrate from Gnulib to glibc so 
that other apps get the fix. Thanks for reporting the issue.

From 06b2e943be39284783ff81ac6c9503200f41dba3 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Sat, 19 Feb 2022 15:04:43 -0800
Subject: [PATCH] mktime: improve heuristic for ca-1986 Indiana DST

Problem reported by Mark Krenz <https://bugs.gnu.org/48085>.
* lib/mktime.c (__mktime_internal): Be more generous about
accepting arguments with the wrong value of tm_isdst, by falling
back to a one-hour DST difference if we find no nearby DST that is
unusual.  This fixes a problem where "1986-04-28 00:00 EDT" was
rejected when TZ="America/Indianapolis" because the nearest DST
timestamp occurred in 1970, a temporal distance too great for the
old heuristic.  This also also narrows the search a bit, which
is a minor performance win.
* m4/mktime.m4 (gl_FUNC_MKTIME_WORKS):
Check for putenv failures and for Bug#48085.
* tests/test-parse-datetime.c (main):
Test for setenv failures and for Bug#48085.
---
 ChangeLog                   | 17 +++++++++++++++++
 lib/mktime.c                | 28 ++++++++++++++++++++--------
 m4/mktime.m4                | 29 +++++++++++++++++++++++++----
 tests/test-parse-datetime.c | 21 +++++++++++++++++++--
 4 files changed, 81 insertions(+), 14 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 4bf0cec7f0..4d56be83d4 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,20 @@
+2022-02-19  Paul Eggert  <[email-masked]>
+
+	mktime: improve heuristic for ca-1986 Indiana DST
+	Problem reported by Mark Krenz <https://bugs.gnu.org/48085>.
+	* lib/mktime.c (__mktime_internal): Be more generous about
+	accepting arguments with the wrong value of tm_isdst, by falling
+	back to a one-hour DST difference if we find no nearby DST that is
+	unusual.  This fixes a problem where "1986-04-28 00:00 EDT" was
+	rejected when TZ="America/Indianapolis" because the nearest DST
+	timestamp occurred in 1970, a temporal distance too great for the
+	old heuristic.  This also also narrows the search a bit, which
+	is a minor performance win.
+	* m4/mktime.m4 (gl_FUNC_MKTIME_WORKS):
+	Check for putenv failures and for Bug#48085.
+	* tests/test-parse-datetime.c (main):
+	Test for setenv failures and for Bug#48085.
+
 2022-02-12  Paul Eggert  <[email-masked]>
 
 	filevercmp: fix several unexpected results
diff --git a/lib/mktime.c b/lib/mktime.c
index aa12e28e16..7dc9d67ef9 100644
--- a/lib/mktime.c
+++ b/lib/mktime.c
@@ -429,8 +429,13 @@ __mktime_internal (struct tm *tp,
 	 time with the right value, and use its UTC offset.
 
 	 Heuristic: probe the adjacent timestamps in both directions,
-	 looking for the desired isdst.  This should work for all real
-	 time zone histories in the tz database.  */
+	 looking for the desired isdst.  If none is found within a
+	 reasonable duration bound, assume a one-hour DST difference.
+	 This should work for all real time zone histories in the tz
+	 database.  */
+
+      /* +1 if we wanted standard time but got DST, -1 if the reverse.  */
+      int dst_difference = (isdst == 0) - (tm.tm_isdst == 0);
 
       /* Distance between probes when looking for a DST boundary.  In
 	 tzdata2003a, the shortest period of DST is 601200 seconds
@@ -441,12 +446,14 @@ __mktime_internal (struct tm *tp,
 	 periods when probing.  */
       int stride = 601200;
 
-      /* The longest period of DST in tzdata2003a is 536454000 seconds
-	 (e.g., America/Jujuy starting 1946-10-01 01:00).  The longest
-	 period of non-DST is much longer, but it makes no real sense
-	 to search for more than a year of non-DST, so use the DST
-	 max.  */
-      int duration_max = 536454000;
+      /* In TZDB 2021e, the longest period of DST (or of non-DST), in
+	 which the DST (or adjacent DST) difference is not one hour,
+	 is 457243209 seconds: e.g., America/Cambridge_Bay with leap
+	 seconds, starting 1965-10-31 00:00 in a switch from
+	 double-daylight time (-05) to standard time (-07), and
+	 continuing to 1980-04-27 02:00 in a switch from standard time
+	 (-07) to daylight time (-06).  */
+      int duration_max = 457243209;
 
       /* Search in both directions, so the maximum distance is half
 	 the duration; add the stride to avoid off-by-1 problems.  */
@@ -483,6 +490,11 @@ __mktime_internal (struct tm *tp,
 	      }
 	  }
 
+      /* No unusual DST offset was found nearby.  Assume one-hour DST.  */
+      t += 60 * 60 * dst_difference;
+      if (mktime_min <= t && t <= mktime_max && convert_time (convert, t, &tm))
+	goto offset_found;
+
       __set_errno (EOVERFLOW);
       return -1;
     }
diff --git a/m4/mktime.m4 b/m4/mktime.m4
index d48f40d187..431b17dcb0 100644
--- a/m4/mktime.m4
+++ b/m4/mktime.m4
@@ -1,4 +1,4 @@
-# serial 36
+# serial 37
 dnl Copyright (C) 2002-2003, 2005-2007, 2009-2022 Free Software Foundation,
 dnl Inc.
 dnl This file is free software; the Free Software Foundation
@@ -82,7 +82,8 @@ spring_forward_gap ()
      instead of "TZ=America/Vancouver" in order to detect the bug even
      on systems that don't support the Olson extension, or don't have the
      full zoneinfo tables installed.  */
-  putenv ("TZ=PST8PDT,M4.1.0,M10.5.0");
+  if (putenv ("TZ=PST8PDT,M4.1.0,M10.5.0") != 0)
+    return -1;
 
   tm.tm_year = 98;
   tm.tm_mon = 3;
@@ -170,7 +171,8 @@ year_2050_test ()
      instead of "TZ=America/Vancouver" in order to detect the bug even
      on systems that don't support the Olson extension, or don't have the
      full zoneinfo tables installed.  */
-  putenv ("TZ=PST8PDT,M4.1.0,M10.5.0");
+  if (putenv ("TZ=PST8PDT,M4.1.0,M10.5.0") != 0)
+    return -1;
 
   t = mktime (&tm);
 
@@ -181,6 +183,25 @@ year_2050_test ()
           || (0 < t && answer - 120 <= t && t <= answer + 120));
 }
 
+static int
+indiana_test ()
+{
+  if (putenv ("TZ=America/Indiana/Indianapolis") != 0)
+    return -1;
+  struct tm tm;
+  tm.tm_year = 1986 - 1900; tm.tm_mon = 4 - 1; tm.tm_mday = 28;
+  tm.tm_hour = 16; tm.tm_min = 24; tm.tm_sec = 50; tm.tm_isdst = 0;
+  time_t std = mktime (&tm);
+  if (! (std == 515107490 || std == 515107503))
+    return 1;
+
+  /* This platform supports TZDB, either without or with leap seconds.
+     Return true if GNU Bug#48085 is absent.  */
+  tm.tm_isdst = 1;
+  time_t dst = mktime (&tm);
+  return std - dst == 60 * 60;
+}
+
 int
 main ()
 {
@@ -236,7 +257,7 @@ main ()
     result |= 16;
   if (! spring_forward_gap ())
     result |= 32;
-  if (! year_2050_test ())
+  if (! year_2050_test () || ! indiana_test ())
     result |= 64;
   return result;
 }]])],
diff --git a/tests/test-parse-datetime.c b/tests/test-parse-datetime.c
index 1e7955bc96..4310ee8a3d 100644
--- a/tests/test-parse-datetime.c
+++ b/tests/test-parse-datetime.c
@@ -126,7 +126,7 @@ main (_GL_UNUSED int argc, char **argv)
      should disable any leap second support.  Otherwise, there will be
      a problem with glibc on sites that default to leap seconds; see
      <https://bugs.gnu.org/12206>.  */
-  setenv ("TZ", "EST5EDT,M3.2.0,M11.1.0", 1);
+  ASSERT (setenv ("TZ", "EST5EDT,M3.2.0,M11.1.0", 1) == 0);
 
   gmtoff = gmt_offset (ref_time);
 
@@ -375,8 +375,25 @@ main (_GL_UNUSED int argc, char **argv)
   ASSERT (result.tv_sec == result2.tv_sec
           && result.tv_nsec == result2.tv_nsec);
 
+  /* If this platform has TZDB, check for GNU Bug#48085.  */
+  ASSERT (setenv ("TZ", "America/Indiana/Indianapolis", 1) == 0);
+  now.tv_sec = 1619641490;
+  now.tv_nsec = 0;
+  struct tm *tm = localtime (&now.tv_sec);
+  if (tm && tm->tm_year == 2021 - 1900 && tm->tm_mon == 4 - 1
+      && tm->tm_mday == 28 && tm->tm_hour == 16 && tm->tm_min == 24
+      && 0 < tm->tm_isdst)
+    {
+      int has_leap_seconds = tm->tm_sec != now.tv_sec % 60;
+      p = "now - 35 years";
+      ASSERT (parse_datetime (&result, p, &now));
+      LOG (p, now, result);
+      ASSERT (result.tv_sec
+              == 515107490 - 60 * 60 + (has_leap_seconds ? 13 : 0));
+    }
+
   /* Check that some "next Monday", "last Wednesday", etc. are correct.  */
-  setenv ("TZ", "UTC0", 1);
+  ASSERT (setenv ("TZ", "UTC0", 1) == 0);
   for (i = 0; day_table[i]; i++)
     {
       unsigned int thur2 = 7 * 24 * 3600; /* 2nd thursday */
-- 
2.32.0