y2038 time checks have overflow checks. Added documentation and
H.Merijn Brand [Sat, 18 Oct 2008 15:26:02 +0000 (15:26 +0000)]
test programs in Porting/

p4raw-id: //depot/perl@34504

Configure
Porting/README.y2038 [new file with mode: 0644]
Porting/timecheck.c [new file with mode: 0644]
Porting/timecheck2.c [new file with mode: 0644]

index 42baaba..0d54123 100755 (executable)
--- a/Configure
+++ b/Configure
@@ -25,7 +25,7 @@
 
 # $Id: Head.U 6 2006-08-25 22:21:46Z rmanfredi $
 #
-# Generated on Fri Oct  3 17:54:11 CEST 2008 [metaconfig 3.5 PL0]
+# Generated on Sat Oct 18 17:13:43 CEST 2008 [metaconfig 3.5 PL0]
 # (with additional metaconfig patches by perlbug@perl.org)
 
 cat >c1$$ <<EOF
@@ -20069,10 +20069,12 @@ int i;
 struct tm *tmp;
 time_t pt;
 
-void gm_check (time_t t)
+void gm_check (time_t t, int min_year, int max_year)
 {
     tmp = gmtime (&t);
-    if (tmp == NULL || tmp->tm_year < -1900)
+    if ( tmp == NULL ||
+       /* Check tm_year overflow */
+        tmp->tm_year < min_year || tmp->tm_year > max_year)
        tmp = NULL;
     else
        pt = t;
@@ -20083,31 +20085,31 @@ int check_max ()
     tmp = NULL;
     pt  = 0;
 #ifdef MAXLONG
-    gm_check (MAXLONG);
+    gm_check (MAXLONG, 69, 0x7fffffff);
 #endif
     if (tmp == NULL || tmp->tm_year < 0) {
        for (i = 63; i >= 0; i--) {
            time_t x = pt | ((time_t)1 << i);
-           if (x < 0) continue;
-           gm_check (x);
+           if (x < 0 || x < pt) continue;
+           gm_check (x, 69, 0x7fffffff);
            }
        }
     printf ("sGMTIME_max=%ld\n", pt);
     return (0);
-  } /* check_max */
+    } /* check_max */
 
 int check_min ()
 {
     tmp = NULL;
     pt  = 0;
 #ifdef MINLONG
-    gm_check (MINLONG);
+    gm_check (MINLONG, -1900, 70);
 #endif
     if (tmp == NULL) {
        for (i = 36; i >= 0; i--) {
            time_t x = pt - ((time_t)1 << i);
            if (x > 0) continue;
-           gm_check (x);
+           gm_check (x, -1900, 70);
            }
        }
     printf ("sGMTIME_min=%ld\n", pt);
@@ -20146,13 +20148,15 @@ int i;
 struct tm *tmp;
 time_t pt;
 
-void local_check (time_t t)
+void local_check (time_t t, int min_year, int max_year)
 {
     if (sizeof (time_t) > 4 && t > 0x7ffffffffffff000LL)
        tmp = NULL;
     else
        tmp = localtime (&t);
-    if (tmp == NULL || tmp->tm_year < -1900)
+    if ( tmp == NULL ||
+       /* Check tm_year overflow */
+        tmp->tm_year < min_year || tmp->tm_year > max_year)
        tmp = NULL;
     else
        pt = t;
@@ -20163,31 +20167,31 @@ int check_max ()
     tmp = NULL;
     pt  = 0;
 #ifdef MAXLONG
-    local_check (MAXLONG);
+    local_check (MAXLONG, 69, 0x7fffffff);
 #endif
     if (tmp == NULL || tmp->tm_year < 0) {
        for (i = 63; i >= 0; i--) {
            time_t x = pt | ((time_t)1 << i);
-           if (x < 0) continue;
-           local_check (x);
+           if (x < 0 || x < pt) continue;
+           local_check (x, 69, 0x7fffffff);
            }
        }
     printf ("sLOCALTIME_max=%ld\n", pt);
     return (0);
-  } /* check_max */
+   } /* check_max */
 
 int check_min ()
 {
     tmp = NULL;
     pt  = 0;
 #ifdef MINLONG
-    local_check (MINLONG);
+    local_check (MINLONG, -1900, 70);
 #endif
     if (tmp == NULL) {
        for (i = 36; i >= 0; i--) {
            time_t x = pt - ((time_t)1 << i);
            if (x > 0) continue;
-           local_check (x);
+           local_check (x, -1900, 70);
            }
        }
     printf ("sLOCALTIME_min=%ld\n", pt);
diff --git a/Porting/README.y2038 b/Porting/README.y2038
new file mode 100644 (file)
index 0000000..a3530d2
--- /dev/null
@@ -0,0 +1,84 @@
+The y2038 implementation for perl
+===========================================================================
+This is an implementation of POSIX time.h which solves the year 2038 bug on
+systems where time_t is only 32 bits.  It is implemented in bog-standard
+ANSI C.  The latest version can be found at http://y2038.googlecode.com/
+
+It makes use of the system's native 32 bit functions to perform time zone
+and daylight savings time calculations and thus does *not* need to ship its
+own time zone table.
+
+time64.h currently implements three public functions, localtime64_r(),
+gmtime64_r() and timegm64().  They are implementations of localtime_r(),
+gmtime_r() and timegm64().
+
+To install, simply copy time64.c and time64.h into your project and make
+use of the functions.
+
+To test, run "make test".  You must have Perl, prove (which comes with a
+recent version of the Test::Harness Perl module) and bzdiff installed to
+run the full test suite.  It will do a number of unit tests, plus test
+against a large table of known good values in different time zones.
+
+Limitations, Issues, etc...
+---------------------------
+localtime64_r() gets its time zone and daylight savings time information by
+mappping the future year back to a similar one between 2010 and 2037, safe
+for localtime_r().  The calculations are accurate according to current time
+zone and daylight savings information, but may become inaccurate if a
+change is made that takes place after 2010.
+
+Future versions will probe for a 64 bit safe system localtime_r() and
+gmtime_r() and use that.
+
+The maximum date is still limited by your tm struct.  Most 32 bit systems
+use a signed integer tm_year which means the practical upper limit is the
+year 2147483647 which is somewhere around 2**54.  You can use a 64 bit
+clean tm struct by setting USE_TM64 in time64.h
+
+Portability
+-----------
+I would like to add some configuration detection stuff in the future, but
+for now all I can do is document the assumptions...
+
+This code assumes that long longs are 64 bit integers which is technically
+in violation of the C standard.  This can be changed in time64.h by
+changing the Time64_T and Int64 typedefs.
+
+There are a number of configuration options in time64.h.
+
+Configure variables
+-------------------
+Configure probes for the maximum and minimum values that gmtime () and
+localtime () accept on the local system. Configure however is only used on
+unix-like systems. For windows and VMS these values are hard-coded. You can
+use timecheck.c in the Porting directory to check those values yourself,
+using the same technique that is used in Configure based on bit-shifting:
+
+  $ cd perl/Porting
+  $ cc -O -o timecheck timecheck.c
+  $ ./timecheck
+  ======================
+  Sizeof time_t = 8
+  gmtime () boundaries:
+    8:  0x00f0c2ab7c54a97f:   2147485547-12-31 23:59:59
+    8: -0x0000000e79747c00:            0-01-01 00:00:00
+  localtime () boundaries:
+    8:  0x00f0c2ab7c549b6f:   2147485547-12-31 23:59:59
+    8: -0x0000000e79748094:            0-01-01 00:00:00
+  Configure variables:
+  sGMTIME_max='67768036191676799'
+  sGMTIME_min='-62167219200'
+  sLOCALTIME_max='67768036191673199'
+  sLOCALTIME_min='-62167220372'
+
+In the rare case that your system uses a double for time_t, you can use the
+alternate approach to test for these values:
+
+  $ cd perl/Porting
+  $ cc -O -o timecheck2{,.c}
+  $ ./timecheck2
+                gmtime max 67768036191676800
+             localtime max 67768036191673200
+                gmtime min -67768040609740800
+             localtime min -67768040609741968
diff --git a/Porting/timecheck.c b/Porting/timecheck.c
new file mode 100644 (file)
index 0000000..07f5872
--- /dev/null
@@ -0,0 +1,148 @@
+#include <sys/types.h>
+#include <stdio.h>
+#include <time.h>
+#include <errno.h>
+#include <values.h>
+
+int opt_v = 0;
+int i;
+struct tm *tmp;
+time_t pt, pt_max, pt_min;
+
+static char hexbuf[80];
+char *hex (time_t t)
+{
+    if ((long long)t < 0)
+       sprintf (hexbuf, " -0x%016lx", -t);
+    else
+       sprintf (hexbuf, "  0x%016lx",  t);
+    return (hexbuf);
+    } /* hex */
+
+void gm_check (time_t t, int min_year, int max_year)
+{
+    tmp = gmtime (&t);
+    if ( tmp == NULL ||
+       /* Check tm_year overflow */
+        tmp->tm_year < min_year || tmp->tm_year > max_year) {
+       if (opt_v)
+           fprintf (stderr, "gmtime (%ld) failed with errno %d\n", t, errno);
+       }
+    else {
+       if (opt_v)
+           fprintf (stderr, "%3d:%s: %12ld-%02d-%02d %02d:%02d:%02d\n",
+               i, hex (t),
+               tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_mday,
+               tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
+       pt = t;
+       }
+    } /* gm_check */
+
+int check_gm_max ()
+{
+    tmp = NULL;
+    pt  = 0;
+    if (tmp == NULL || tmp->tm_year < 0) {
+       for (i = 63; i >= 0; i--) {
+           time_t x = pt | ((time_t)1 << i);
+           if (x < 0 || x < pt) continue;
+           gm_check (x, 69, 0x7fffffff);
+           }
+       }
+    pt_max = pt;
+    return (0);
+    } /* check_gm_max */
+
+int check_gm_min ()
+{
+    tmp = NULL;
+    pt  = 0;
+    if (tmp == NULL) {
+       for (i = 36; i >= 0; i--) {
+           time_t x = pt - ((time_t)1 << i);
+           if (x > 0) continue;
+           gm_check (x, -1900, 70);
+           }
+       }
+    pt_min = pt;
+    return (0);
+    } /* check_gm_min */
+
+void lt_check (time_t t, int min_year, int max_year)
+{
+    if (sizeof (time_t) > 4 && t > 0x7ffffffffffff000LL)
+       tmp = NULL;
+    else
+       tmp = localtime (&t);
+    if ( tmp == NULL ||
+       /* Check tm_year overflow */
+        tmp->tm_year < min_year || tmp->tm_year > max_year) {
+       if (opt_v)
+           fprintf (stderr, "localtime (%ld) failed with errno %d\n", t, errno);
+       }
+    else {
+       if (opt_v)
+           fprintf (stderr, "%3d:%s: %12ld-%02d-%02d %02d:%02d:%02d\n",
+               i, hex (t),
+               tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_mday,
+               tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
+       pt = t;
+       }
+    } /* lt_check */
+
+int check_lt_max ()
+{
+    tmp = NULL;
+    pt  = 0;
+    if (tmp == NULL || tmp->tm_year < 0) {
+       for (i = 63; i >= 0; i--) {
+           time_t x = pt | ((time_t)1 << i);
+           if (x < 0 || x < pt) continue;
+           lt_check (x, 69, 0x7fffffff);
+           }
+       }
+    pt_max = pt;
+    return (0);
+    } /* check_lt_max */
+
+int check_lt_min ()
+{
+    tmp = NULL;
+    pt  = 0;
+    if (tmp == NULL) {
+       for (i = 36; i >= 0; i--) {
+           time_t x = pt - ((time_t)1 << i);
+           if (x > 0) continue;
+           lt_check (x, -1900, 70);
+           }
+       }
+    pt_min = pt;
+    return (0);
+    } /* check_lt_min */
+
+int main (int argc, char *argv[])
+{
+    time_t gm_max, gm_min, lt_max, lt_min;
+    if (argc > 1 && strcmp (argv[1], "-v") == 0) opt_v++;
+
+    check_gm_max (); gm_max = pt_max;
+    check_gm_min (); gm_min = pt_min;
+    check_lt_max (); lt_max = pt_max;
+    check_lt_min (); lt_min = pt_min;
+
+    opt_v++;
+    printf ("======================\n");
+    printf ("Sizeof time_t = %ld\n", (i = sizeof (time_t)));
+    printf ("gmtime () boundaries:\n");
+    gm_check (gm_max,    69, 0x7fffffff);
+    gm_check (gm_min, -1900,         70);
+    printf ("localtime () boundaries:\n");
+    lt_check (lt_max,    69, 0x7fffffff);
+    lt_check (lt_min, -1900,         70);
+    printf ("Configure variables:\n");
+    printf ("sGMTIME_max='%ld'\n",    gm_max);
+    printf ("sGMTIME_min='%ld'\n",    gm_min);
+    printf ("sLOCALTIME_max='%ld'\n", lt_max);
+    printf ("sLOCALTIME_min='%ld'\n", lt_min);
+    return (0);
+    } /* main */
diff --git a/Porting/timecheck2.c b/Porting/timecheck2.c
new file mode 100644 (file)
index 0000000..a4445ef
--- /dev/null
@@ -0,0 +1,112 @@
+/* A little program to test the limits of your system's time functions */
+
+#include <time.h>
+#include <stdio.h>
+#include <math.h>
+
+time_t Time_Zero = 0;
+
+/* Visual C++ 2008's difftime() can't do negative times */
+double my_difftime(time_t left, time_t right) {
+       double diff = (double)left - (double)right;
+       return diff;
+}
+
+void check_date_max( struct tm * (*date_func)(const time_t *), char *func_name ) {
+    struct tm *date;
+    time_t time = 0;
+    time_t last_time = 0;
+    time_t time_change;
+    int i;
+
+    for (i = 0; i <= 63; i++) {
+        date = (*date_func)(&time);
+
+        /* date_func() broke or tm_year overflowed */
+        if(date == NULL || date->tm_year < 69)
+          break;
+
+        last_time = time;
+        time += time + 1;
+
+        /* time_t overflowed */
+        if( time < last_time )
+            break;
+    }
+
+    /* Binary search for the exact failure point */
+    time = last_time;
+    time_change = last_time / 2;
+
+    do {
+        time += time_change;
+
+        date = (*date_func)(&time);
+
+        /* date_func() broke or tm_year overflowed or time_t overflowed */
+        if(date == NULL || date->tm_year < 69 || time < last_time) {
+            time = last_time;
+            time_change = time_change / 2;
+        }
+        else {
+            last_time = time;
+        }
+    } while(time_change > 0);
+
+    printf("%20s max %.0f\n", func_name, my_difftime(last_time, Time_Zero));
+}
+
+
+void check_date_min( struct tm * (*date_func)(const time_t *), char *func_name ) {
+    struct tm *date;
+    time_t time = -1;
+    time_t last_time = 0;
+    time_t time_change;
+    int i;
+
+    for (i = 1; i <= 63; i++) {
+        date = (*date_func)(&time);
+
+        /* date_func() broke or tm_year underflowed */
+        if(date == NULL || date->tm_year > 70)
+            break;
+
+        last_time = time;
+        time += time;
+
+        /* time_t underflowed */
+        if( time > last_time )
+            break;
+    }
+
+    /* Binary search for the exact failure point */
+    time = last_time;
+    time_change = last_time / 2;
+
+    do {
+        time += time_change;
+
+        date = (*date_func)(&time);
+
+        /* gmtime() broke or tm_year overflowed or time_t overflowed */
+        if(date == NULL || date->tm_year > 70 || time > last_time) {
+            time = last_time;
+            time_change = time_change / 2;
+        }
+        else {
+            last_time = time;
+        }
+    } while(time_change < 0);
+
+    printf("%20s min %.0f\n", func_name, my_difftime(last_time, Time_Zero));
+}
+
+
+int main(void) {
+    check_date_max(gmtime, "gmtime");
+    check_date_max(localtime, "localtime");
+    check_date_min(gmtime, "gmtime");
+    check_date_min(localtime, "localtime");
+
+    return 0;
+}