]> git.ipfire.org Git - thirdparty/ntp.git/commitdiff
[Bug 1011] gmtime() returns NULL on windows where it would not under Unix
authorHarlan Stenn <stenn@ntp.org>
Tue, 11 Nov 2008 21:49:27 +0000 (16:49 -0500)
committerHarlan Stenn <stenn@ntp.org>
Tue, 11 Nov 2008 21:49:27 +0000 (16:49 -0500)
bk: 4919fde7iijILbN9Ll0RjlpKZjniGA

ChangeLog
libntp/caljulian.c
libntp/prettydate.c

index 4c979d2d003073036285c36e99d9ff5f313c3416..1623be496091dd3d7645b69f346f179b675a7f7c 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,5 @@
+* [Bug 1011] gmtime() returns NULL on windows where it would not under Unix.
+* Updated caljulian.c and prettydate.c from Juergen Perlinger.
 (4.2.5p139) 2008/11/11 Released by Harlan Stenn <stenn@ntp.org>
 * Typo fix to driver20.html.
 (4.2.5p138) 2008/11/10 Released by Harlan Stenn <stenn@ntp.org>
index 8d5c6e187229b6a7710be18329ba2ded8f6c6645..158038de555fe4136211107ccfeba89807709df2 100644 (file)
@@ -7,25 +7,29 @@
 #include "ntp_calendar.h"
 #include "ntp_stdlib.h"
 #include "ntp_fp.h"
+#include "ntp_unixtime.h"
+
+#ifdef HAVE_LIMITS_H
+# include <limits.h>
+#endif
+
+#if !(defined(ISC_CHECK_ALL) || defined(ISC_CHECK_NONE) || \
+      defined(ISC_CHECK_ENSURE) || defined(ISC_CHECK_INSIST) || \
+      defined(ISC_CHECK_INVARIANT))
+# define ISC_CHECK_ALL
+#endif
+
 #include "ntp_assert.h"
 
-#if 0
-/*
- * calmonthtab - days-in-the-month table
+#if 1
+
+/* Updated 2008-11-10 Juergen Perlinger <juergen.perlinger@t-online.de>
+ *
+ * Make the conversion 2038-proof with proper NTP epoch unfolding and extended
+ * precision calculations. Though we should really get a 'time_t' with more
+ * than 32 bits at least until 2037, because the unfolding cannot work after
+ * the wrap of the 32-bit 'time_t'.
  */
-static u_short calmonthtab[11] = {
-       JAN,
-       FEB,
-       MAR,
-       APR,
-       MAY,
-       JUN,
-       JUL,
-       AUG,
-       SEP,
-       OCT,
-       NOV
-};
 
 void
 caljulian(
@@ -33,89 +37,120 @@ caljulian(
        register struct calendar        *jt
        )
 {
-       u_long ntp_day;
-       u_long minutes;
+       u_long  saved_time = ntptime;
+       u_long  ntp_day; /* days (since christian era or in year) */ 
+       u_long  n400;    /* # of Gregorian cycles */
+       u_long  n100;    /* # of normal centuries */
+       u_long  n4;      /* # of 4-year cycles */
+       u_long  n1;      /* # of years into a leap year cycle */
+       u_long  sclday;  /* scaled days for month conversion */
+       int     leaps;   /* # of leaps days in year */
+       time_t  now;     /* current system time */
+       u_int32 tmplo;   /* double precision tmp value / lo part */
+       int32   tmphi;   /* double precision tmp value / hi part */
+
+       NTP_INSIST(NULL != jt);
+
        /*
-        * Absolute, zero-adjusted Christian era day, starting from the
-        * mythical day 12/1/1 BC
+        * First we have to unfold the ntp time stamp around the current time
+        * to make sure we are in the right epoch. Also we we do *NOT* fold
+        * before the begin of the first NTP epoch, so we WILL have a
+        * non-negative time stamp afterwards. Though at the time of this
+        * writing (2008 A.D.) it would be really strange to have systems
+        * running with clock set to he 1960's or before...
         */
-       u_long acez_day;
+       now   = time(NULL);
+       tmplo = (u_int32)now;
+       tmphi = (int32)(now >> 16 >> 16);
+       
+       M_ADD(tmphi, tmplo, 1, LONG_MAX + JAN_1970);
+       if ((ntptime > tmplo) && (tmphi > 0))
+               --tmphi;
+       tmplo = ntptime;
+       
+       /*
+        * Now split into days and seconds-of-day, using the fact that
+        * SECSPERDAY (86400) == 675 * 128; we can get roughly 17000 years of
+        * time scale, using only 32-bit calculations. Some magic numbers here,
+        * sorry for that.
+        */
+       ntptime  = tmplo & 127; /* save remainder bits */
+       tmplo    = (tmplo >> 7) | (tmphi << 25);
+       ntp_day  =  (u_long)tmplo / 675;
+       ntptime += ((u_long)tmplo % 675) << 7;
 
-       u_long d400;                             /* Days into a Gregorian cycle */
-       u_long d100;                             /* Days into a normal century */
-       u_long d4;                                       /* Days into a 4-year cycle */
-       u_long n400;                             /* # of Gregorian cycles */
-       u_long n100;                             /* # of normal centuries */
-       u_long n4;                                       /* # of 4-year cycles */
-       u_long n1;                                       /* # of years into a leap year */
-                                                /*   cycle */
+       /* some checks for the algorithm */
+       NTP_ENSURE(ntptime < SECSPERDAY);
+       NTP_INVARIANT((u_long)(ntptime + ntp_day * SECSPERDAY) == saved_time);
 
        /*
         * Do the easy stuff first: take care of hh:mm:ss, ignoring leap
         * seconds
         */
        jt->second = (u_char)(ntptime % SECSPERMIN);
-       minutes    = ntptime / SECSPERMIN;
-       jt->minute = (u_char)(minutes % MINSPERHR);
-       jt->hour   = (u_char)((minutes / MINSPERHR) % HRSPERDAY);
+       ntptime   /= SECSPERMIN;
+       jt->minute = (u_char)(ntptime % MINSPERHR);
+       ntptime   /= MINSPERHR;
+       jt->hour   = (u_char)(ntptime);
 
-       /*
-        * Find the day past 1900/01/01 00:00 UTC
-        */
-       ntp_day = ntptime / SECSPERDAY;
-       acez_day = DAY_NTP_STARTS + ntp_day - 1;
-       n400     = acez_day/GREGORIAN_CYCLE_DAYS;
-       d400     = acez_day%GREGORIAN_CYCLE_DAYS;
-       n100     = d400 / GREGORIAN_NORMAL_CENTURY_DAYS;
-       d100     = d400 % GREGORIAN_NORMAL_CENTURY_DAYS;
-       n4               = d100 / GREGORIAN_NORMAL_LEAP_CYCLE_DAYS;
-       d4               = d100 % GREGORIAN_NORMAL_LEAP_CYCLE_DAYS;
-       n1               = d4 / DAYSPERYEAR;
+       /* check time invariants */
+       NTP_ENSURE(jt->second < SECSPERMIN);
+       NTP_ENSURE(jt->minute < MINSPERHR);
+       NTP_ENSURE(jt->hour   < HRSPERDAY);
 
        /*
-        * Calculate the year and year-of-day
+        * Find the day past 1900/01/01 00:00 UTC
         */
-       jt->yearday = (u_short)(1 + d4%DAYSPERYEAR);
-       jt->year        = (u_short)(400*n400 + 100*n100 + n4*4 + n1);
+       ntp_day += DAY_NTP_STARTS - 1;  /* convert to days in CE */
+       n400     = ntp_day / GREGORIAN_CYCLE_DAYS; /* split off cycles */
+       ntp_day %= GREGORIAN_CYCLE_DAYS;
+       n100     = ntp_day / GREGORIAN_NORMAL_CENTURY_DAYS;
+       ntp_day %= GREGORIAN_NORMAL_CENTURY_DAYS;
+       n4       = ntp_day / GREGORIAN_NORMAL_LEAP_CYCLE_DAYS;
+       ntp_day %= GREGORIAN_NORMAL_LEAP_CYCLE_DAYS;
+       n1       = ntp_day / DAYSPERYEAR;
+       ntp_day %= DAYSPERYEAR; /* now zero-based day-of-year */
+
+       NTP_ENSURE(ntp_day < 366);
 
-       if (n100 == 4 || n1 == 4)
-       {
        /*
-        * If the cycle year ever comes out to 4, it must be December 31st
-        * of a leap year.
+        * Calculate the year and day-of-year
         */
-       jt->month        = 12;
-       jt->monthday = 31;
-       jt->yearday  = 366;
+       jt->year = (u_short)(400*n400 + 100*n100 + 4*n4 + n1);
+
+       if ((n100 | n1) > 3) {
+               /*
+                * If the cycle year ever comes out to 4, it must be December
+                * 31st of a leap year.
+                */
+               jt->month    = 12;
+               jt->monthday = 31;
+               jt->yearday  = 366;
+       } else {
+               /*
+                * The following code is according to the excellent book
+                * 'Calendrical Calculations' by Nachum Dershowitz and Edward
+                * Reingold. It converts the day-of-year into month and
+                * day-of-month, using a linear transformation with integer
+                * truncation. Magic numbers again, but they will not be used
+                * anywhere else.
+                */
+               sclday = ntp_day * 7 + 217;
+               leaps  = ((n1 == 3) && ((n4 != 24) || (n100 == 3))) ? 1 : 0;
+               if (ntp_day >= JAN + FEB + leaps)
+                       sclday += (2 - leaps) * 7;
+               ++jt->year;
+               jt->month    = sclday / 214;
+               jt->monthday = (u_char)((sclday % 214) / 7 + 1);
+               jt->yearday  = (u_short)(1 + ntp_day);
        }
-       else
-       {
-       /*
-        * Else, search forwards through the months to get the right month
-        * and date.
-        */
-       int monthday;
-
-       jt->year++;
-       monthday = jt->yearday;
 
-       for (jt->month=0;jt->month<11; jt->month++)
-       {
-               int t;
-
-               t = monthday - calmonthtab[jt->month];
-               if (jt->month == 1 && is_leapyear(jt->year))
-               t--;
-
-               if (t > 0)
-               monthday = t;
-               else
-               break;
-       }
-       jt->month++;
-       jt->monthday = (u_char) monthday;
-       }
+       /* check date invariants */
+       NTP_ENSURE(1 <= jt->month    && jt->month    <=  12); 
+       NTP_ENSURE(1 <= jt->monthday && jt->monthday <=  31);
+       NTP_ENSURE(1 <= jt->yearday  && jt->yearday  <= 366);
 }
+
 #else
 
 /* Updated 2003-12-30 TMa
index e9aa7d84d22efc42bcbbc2572d67cdaeff211fb9..eba71a901c286098f70fe024afecbd304fdd8c79 100644 (file)
@@ -9,6 +9,10 @@
 #include "ntp_stdlib.h"
 #include "ntp_assert.h"
 
+#ifdef HAVE_LIMITS_H
+# include <limits.h>
+#endif
+
 static const char *months[] = {
   "Jan", "Feb", "Mar", "Apr", "May", "Jun",
   "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
@@ -20,55 +24,119 @@ static const char *days[] = {
 
 /* Helper function to handle possible wraparound of the ntp epoch.
 
-   Works by assuming that the localtime/gmtime library functions 
-   have been updated so that they work
+   Works by periodic extension of the ntp time stamp in the NTP epoch.  If the
+   'time_t' is 32 bit, use solar cycle warping to get the value in a suitable
+   range. Also uses solar cycle warping to work around really buggy
+   implementations of 'gmtime()' / 'localtime()' that cannot work with a
+   negative time value, that is, times before 1970-01-01. (MSVCRT...)
+
+   Apart from that we're assuming that the localtime/gmtime library functions
+   have been updated so that they work...
 */
 
-#define MAX_EPOCH_NR 1000
+
+/* solar cycle in secs, unsigned secs and years. And the cycle limits.
+**
+** And an explanation. The julian calendar repeats ever 28 years, because it's
+** the LCM of 7 and 4, the week and leap year cycles. This is called a 'solar
+** cycle'. The gregorian calendar does the same as long as no centennial year
+** (divisible by 100, but not 400) goes in the way. So between 1901 and 2099
+** (inclusive) we can warp time stamps by 28 years to make them suitable for
+** localtime() and gmtime() if we have trouble. Of course this will play
+** hubbubb with the DST zone switches, so we should do it only if necessary;
+** but as we NEED a proper conversion to dates via gmtime() we should try to
+** cope with as many idiosyncrasies as possible.
+*/
+#define SOLAR_CYCLE_SECS   0x34AADC80UL        /* 7*1461*86400*/
+#define SOLAR_CYCLE_YEARS  28
+#define MINFOLD -3
+#define MAXFOLD  3
 
 struct tm *
 ntp2unix_tm(
        u_long ntp, int local
        )
 {
-       time_t t, curr;
        struct tm *tm;
-       int curr_year, epoch_nr;
-
-       /* First get the current year: */
-       curr = time(NULL);
-       tm = local ? localtime(&curr) : gmtime(&curr);
-       if (!tm) return NULL;
-
-       curr_year = 1900 + tm->tm_year;
+       int32      folds = 0;
+       time_t     t     = time(NULL);
+       u_int32    dwlo  = (int32)t; /* might expand for SIZEOF_TIME_T < 4 */
+       int32      dwhi  = (int32)(t >> 16 >> 16);/* double shift: avoid warnings */
+       
+       /* Shift NTP to UN*X epoch, then unfold around currrent time */
+       M_ADD(dwhi, dwlo, 0, LONG_MAX);
+       if ((ntp -= JAN_1970) > dwlo)
+               --dwhi;
+       dwlo = ntp;
+#   if SIZEOF_TIME_T < 4
+#      error sizeof(time_t) < 4 -- this will not work!
+#   elif SIZEOF_TIME_T == 4
+
+       /*
+       ** If the result will not fit into a 'time_t' we have to warp solar
+       ** cycles. That's implemented by looped addition / subtraction with
+       ** M_ADD and M_SUB to avoid implicit 64 bit operations, especially
+       ** division. As he number of warps is rather limited there's no big
+       ** performance loss here.
+       **
+       ** note: unless the high word doesn't match the sign-extended low word,
+       ** the combination will not fit into time_t. That's what we use for
+       ** loop control here...
+       */
+       while (dwhi != ((int32)dwlo >> 31)) {
+               if (dwhi < 0 && --folds >= MINFOLD)
+                       M_ADD(dwhi, dwlo, 0, SOLAR_CYCLE_SECS);
+               else if (dwhi >= 0 && ++folds <= MAXFOLD)
+                       M_SUB(dwhi, dwlo, 0, SOLAR_CYCLE_SECS);
+               else
+                       return NULL;
+       }
 
-       /* Convert the ntp timestamp to a unix utc seconds count: */
-       t = (time_t) ntp - JAN_1970;
+#   else
 
-       /* Check that the ntp timestamp is not before a 136 year window centered
-          around the current year:
+       /* everything fine -- no reduction needed for the next thousand years */
 
-          Failsafe in case of an infinite loop:
-       Allow up to 1000 epochs of 136 years each!
-       */
-    for (epoch_nr = 0; epoch_nr < MAX_EPOCH_NR; epoch_nr++) {
-               tm = local ? localtime(&t) : gmtime(&t);
-               NTP_INSIST(tm != NULL);
+#   endif
 
-#if SIZEOF_TIME_T < 4
-# include "Bletch: sizeof(time_t) < 4!"
-#endif
+       /* combine hi/lo to make time stamp */
+       t = ((time_t)dwhi << 16 << 16) | dwlo;  /* double shift: avoid warnings */
 
-#if SIZEOF_TIME_T == 4
-               /* If 32 bits, then year is 1970-2038, so no sense looking */
-               epoch_nr = MAX_EPOCH_NR;
-#else  /* SIZEOF_TIME_T > 4 */
-               /* Check that the resulting year is in the correct epoch: */
-               if (1900 + tm->tm_year > curr_year - 68) break;
+#   ifdef _MSC_VER     /* make this an autoconf option? */
 
-               /* Epoch wraparound: Add 2^32 seconds! */
-               t += (time_t) 65536 << 16;
-#endif /* SIZEOF_TIME_T > 4 */
+       /*
+       ** The MSDN says that the (Microsoft) Windoze versions of 'gmtime()'
+       ** and 'localtime()' will bark on time stamps < 0. Better to fix it
+       ** immediately.
+       */
+       while (t < 0) { 
+               if (--folds < MINFOLD)
+                       return NULL;
+               t += SOLAR_CYCLE_SECS;
+       }
+                       
+#   endif /* Microsoft specific */
+
+       /* 't' should be a suitable value by now. Just go ahead. */
+       while (!(tm = (*(local ? localtime : gmtime))(&t)))
+               /* seems there are some other pathological implementations of
+               ** 'gmtime()' and 'localtime()' somewhere out there. No matter
+               ** if we have 32-bit or 64-bit 'time_t', try to fix this by
+               ** solar cycle warping again...
+               */
+               if (t < 0) {
+                       if (--folds < MINFOLD)
+                               return NULL;
+                       t += SOLAR_CYCLE_SECS;
+               } else {
+                       if ((++folds > MAXFOLD) || ((t -= SOLAR_CYCLE_SECS) < 0))
+                               return NULL; /* That's truely pathological! */
+               }
+       /* 'tm' surely not NULL here... */
+       if (folds != 0) {
+               tm->tm_year += folds * SOLAR_CYCLE_YEARS;
+               if (tm->tm_year <= 0 || tm->tm_year >= 200)
+                       return NULL;    /* left warp range... can't help here! */
        }
        return tm;
 }