about summary refs log tree commit diff
path: root/timezone/zdump.c
diff options
context:
space:
mode:
Diffstat (limited to 'timezone/zdump.c')
-rw-r--r--timezone/zdump.c453
1 files changed, 300 insertions, 153 deletions
diff --git a/timezone/zdump.c b/timezone/zdump.c
index b532fe3eae..7d99cc74bd 100644
--- a/timezone/zdump.c
+++ b/timezone/zdump.c
@@ -15,7 +15,7 @@
 #include <stdio.h>
 
 #ifndef HAVE_SNPRINTF
-# define HAVE_SNPRINTF (199901 <= __STDC_VERSION__)
+# define HAVE_SNPRINTF (!PORT_TO_C89 || 199901 <= __STDC_VERSION__)
 #endif
 
 #ifndef HAVE_LOCALTIME_R
@@ -35,17 +35,13 @@
 #endif
 
 #ifndef ZDUMP_LO_YEAR
-#define ZDUMP_LO_YEAR	(-500)
+# define ZDUMP_LO_YEAR (-500)
 #endif /* !defined ZDUMP_LO_YEAR */
 
 #ifndef ZDUMP_HI_YEAR
-#define ZDUMP_HI_YEAR	2500
+# define ZDUMP_HI_YEAR 2500
 #endif /* !defined ZDUMP_HI_YEAR */
 
-#ifndef MAX_STRING_LENGTH
-#define MAX_STRING_LENGTH	1024
-#endif /* !defined MAX_STRING_LENGTH */
-
 #define SECSPERNYEAR	(SECSPERDAY * DAYSPERNYEAR)
 #define SECSPERLYEAR	(SECSPERNYEAR + SECSPERDAY)
 #define SECSPER400YEARS	(SECSPERNYEAR * (intmax_t) (300 + 3)	\
@@ -61,7 +57,7 @@
 enum { SECSPER400YEARS_FITS = SECSPERLYEAR <= INTMAX_MAX / 400 };
 
 #if HAVE_GETTEXT
-#include <locale.h>	/* for setlocale */
+# include <locale.h> /* for setlocale */
 #endif /* HAVE_GETTEXT */
 
 #if ! HAVE_LOCALTIME_RZ
@@ -77,7 +73,7 @@ extern int	optind;
 #endif
 
 /* The minimum and maximum finite time values.  */
-enum { atime_shift = CHAR_BIT * sizeof (time_t) - 2 };
+enum { atime_shift = CHAR_BIT * sizeof(time_t) - 2 };
 static time_t const absolute_min_time =
   ((time_t) -1 < 0
    ? (- ((time_t) ~ (time_t) 0 < 0)
@@ -88,22 +84,27 @@ static time_t const absolute_max_time =
    ? (((time_t) 1 << atime_shift) - 1 + ((time_t) 1 << atime_shift))
    : -1);
 static int	longest;
-static char *	progname;
+static char const *progname;
 static bool	warned;
 static bool	errout;
 
 static char const *abbr(struct tm const *);
-static intmax_t	delta(struct tm *, struct tm *) ATTRIBUTE_PURE;
+ATTRIBUTE_REPRODUCIBLE static intmax_t delta(struct tm *, struct tm *);
 static void dumptime(struct tm const *);
-static time_t hunt(timezone_t, char *, time_t, time_t);
+static time_t hunt(timezone_t, time_t, time_t, bool);
 static void show(timezone_t, char *, time_t, bool);
+static void showextrema(timezone_t, char *, time_t, struct tm *, time_t);
 static void showtrans(char const *, struct tm const *, time_t, char const *,
 		      char const *);
 static const char *tformat(void);
-static time_t yeartot(intmax_t) ATTRIBUTE_PURE;
+ATTRIBUTE_REPRODUCIBLE static time_t yeartot(intmax_t);
 
-/* Unlike <ctype.h>'s isdigit, this also works if c < 0 | c > UCHAR_MAX. */
-#define is_digit(c) ((unsigned)(c) - '0' <= 9)
+/* Is C an ASCII digit?  */
+static bool
+is_digit(char c)
+{
+  return '0' <= c && c <= '9';
+}
 
 /* Is A an alphabetic character in the C locale?  */
 static bool
@@ -124,26 +125,49 @@ is_alpha(char a)
 	}
 }
 
-/* Return A + B, exiting if the result would overflow.  */
-static size_t
-sumsize(size_t a, size_t b)
+ATTRIBUTE_NORETURN static void
+size_overflow(void)
 {
-  size_t sum = a + b;
-  if (sum < a) {
-    fprintf(stderr, "%s: size overflow\n", progname);
-    exit(EXIT_FAILURE);
-  }
-  return sum;
+  fprintf(stderr, _("%s: size overflow\n"), progname);
+  exit(EXIT_FAILURE);
+}
+
+/* Return A + B, exiting if the result would overflow either ptrdiff_t
+   or size_t.  A and B are both nonnegative.  */
+ATTRIBUTE_REPRODUCIBLE static ptrdiff_t
+sumsize(ptrdiff_t a, ptrdiff_t b)
+{
+#ifdef ckd_add
+  ptrdiff_t sum;
+  if (!ckd_add(&sum, a, b) && sum <= INDEX_MAX)
+    return sum;
+#else
+  if (a <= INDEX_MAX && b <= INDEX_MAX - a)
+    return a + b;
+#endif
+  size_overflow();
+}
+
+/* Return the size of of the string STR, including its trailing NUL.
+   Report an error and exit if this would exceed INDEX_MAX which means
+   pointer subtraction wouldn't work.  */
+static ptrdiff_t
+xstrsize(char const *str)
+{
+  size_t len = strlen(str);
+  if (len < INDEX_MAX)
+    return len + 1;
+  size_overflow();
 }
 
 /* Return a pointer to a newly allocated buffer of size SIZE, exiting
-   on failure.  SIZE should be nonzero.  */
-static void * ATTRIBUTE_MALLOC
-xmalloc(size_t size)
+   on failure.  SIZE should be positive.  */
+ATTRIBUTE_MALLOC static void *
+xmalloc(ptrdiff_t size)
 {
   void *p = malloc(size);
   if (!p) {
-    perror(progname);
+    fprintf(stderr, _("%s: Memory exhausted\n"), progname);
     exit(EXIT_FAILURE);
   }
   return p;
@@ -204,7 +228,7 @@ localtime_r(time_t *tp, struct tm *tmp)
 # undef localtime_rz
 # define localtime_rz zdump_localtime_rz
 static struct tm *
-localtime_rz(timezone_t rz, time_t *tp, struct tm *tmp)
+localtime_rz(ATTRIBUTE_MAYBE_UNUSED timezone_t rz, time_t *tp, struct tm *tmp)
 {
   return localtime_r(tp, tmp);
 }
@@ -227,33 +251,65 @@ mktime_z(timezone_t tz, struct tm *tmp)
 static timezone_t
 tzalloc(char const *val)
 {
+# if HAVE_SETENV
+  if (setenv("TZ", val, 1) != 0) {
+    char const *e = strerror(errno);
+    fprintf(stderr, _("%s: setenv: %s\n"), progname, e);
+    exit(EXIT_FAILURE);
+  }
+  tzset();
+  return &optarg;  /* Any valid non-null char ** will do.  */
+# else
+  enum { TZeqlen = 3 };
+  static char const TZeq[TZeqlen] = "TZ=";
   static char **fakeenv;
-  char **env = fakeenv;
-  char *env0;
-  if (! env) {
-    char **e = environ;
-    int to;
-
-    while (*e++)
-      continue;
-    env = xmalloc(sumsize(sizeof *environ,
-			  (e - environ) * sizeof *environ));
-    to = 1;
-    for (e = environ; (env[to] = *e); e++)
-      to += strncmp(*e, "TZ=", 3) != 0;
+  static ptrdiff_t fakeenv0size;
+  void *freeable = NULL;
+  char **env = fakeenv, **initial_environ;
+  ptrdiff_t valsize = xstrsize(val);
+  if (fakeenv0size < valsize) {
+    char **e = environ, **to;
+    ptrdiff_t initial_nenvptrs = 1;  /* Counting the trailing NULL pointer.  */
+
+    while (*e++) {
+#  ifdef ckd_add
+      if (ckd_add(&initial_nenvptrs, initial_nenvptrs, 1)
+	  || INDEX_MAX < initial_nenvptrs)
+	size_overflow();
+#  else
+      if (initial_nenvptrs == INDEX_MAX / sizeof *environ)
+	size_overflow();
+      initial_nenvptrs++;
+#  endif
+    }
+    fakeenv0size = sumsize(valsize, valsize);
+    fakeenv0size = max(fakeenv0size, 64);
+    freeable = env;
+    fakeenv = env =
+      xmalloc(sumsize(sumsize(sizeof *environ,
+			      initial_nenvptrs * sizeof *environ),
+		      sumsize(TZeqlen, fakeenv0size)));
+    to = env + 1;
+    for (e = environ; (*to = *e); e++)
+      to += strncmp(*e, TZeq, TZeqlen) != 0;
+    env[0] = memcpy(to + 1, TZeq, TZeqlen);
   }
-  env0 = xmalloc(sumsize(sizeof "TZ=", strlen(val)));
-  env[0] = strcat(strcpy(env0, "TZ="), val);
-  environ = fakeenv = env;
+  memcpy(env[0] + TZeqlen, val, valsize);
+  initial_environ = environ;
+  environ = env;
   tzset();
-  return env;
+  free(freeable);
+  return initial_environ;
+# endif
 }
 
 static void
-tzfree(timezone_t env)
+tzfree(ATTRIBUTE_MAYBE_UNUSED timezone_t initial_environ)
 {
-  environ = env + 1;
-  free(env[0]);
+# if !HAVE_SETENV
+  environ = initial_environ;
+  tzset();
+# endif
 }
 #endif /* ! USE_LOCALTIME_RZ */
 
@@ -263,11 +319,25 @@ static void
 gmtzinit(void)
 {
   if (USE_LOCALTIME_RZ) {
-    static char const utc[] = "UTC0";
-    gmtz = tzalloc(utc);
+    /* Try "GMT" first to find out whether this is one of the rare
+       platforms where time_t counts leap seconds; this works due to
+       the "Zone GMT 0 - GMT" line in the "etcetera" file.  If "GMT"
+       fails, fall back on "GMT0" which might be similar due to the
+       "Link GMT GMT0" line in the "backward" file, and which
+       should work on all POSIX platforms.  The rest of zdump does not
+       use the "GMT" abbreviation that comes from this setting, so it
+       is OK to use "GMT" here rather than the modern "UTC" which
+       would not work on platforms that omit the "backward" file.  */
+    gmtz = tzalloc("GMT");
     if (!gmtz) {
-      perror(utc);
-      exit(EXIT_FAILURE);
+      static char const gmt0[] = "GMT0";
+      gmtz = tzalloc(gmt0);
+      if (!gmtz) {
+	char const *e = strerror(errno);
+	fprintf(stderr, _("%s: unknown timezone '%s': %s\n"),
+		progname, gmt0, e);
+	exit(EXIT_FAILURE);
+      }
     }
   }
 }
@@ -345,23 +415,23 @@ abbrok(const char *const abbrp, const char *const zone)
 
 /* Return a time zone abbreviation.  If the abbreviation needs to be
    saved, use *BUF (of size *BUFALLOC) to save it, and return the
-   abbreviation in the possibly-reallocated *BUF.  Otherwise, just
+   abbreviation in the possibly reallocated *BUF.  Otherwise, just
    return the abbreviation.  Get the abbreviation from TMP.
    Exit on memory allocation failure.  */
 static char const *
-saveabbr(char **buf, size_t *bufalloc, struct tm const *tmp)
+saveabbr(char **buf, ptrdiff_t *bufalloc, struct tm const *tmp)
 {
   char const *ab = abbr(tmp);
   if (HAVE_LOCALTIME_RZ)
     return ab;
   else {
-    size_t ablen = strlen(ab);
-    if (*bufalloc <= ablen) {
+    ptrdiff_t absize = xstrsize(ab);
+    if (*bufalloc < absize) {
       free(*buf);
 
       /* Make the new buffer at least twice as long as the old,
 	 to avoid O(N**2) behavior on repeated calls.  */
-      *bufalloc = sumsize(*bufalloc, ablen + 1);
+      *bufalloc = sumsize(*bufalloc, absize);
 
       *buf = xmalloc(*bufalloc);
     }
@@ -406,7 +476,7 @@ main(int argc, char *argv[])
 {
 	/* These are static so that they're initially zero.  */
 	static char *		abbrev;
-	static size_t		abbrevsize;
+	static ptrdiff_t	abbrevsize;
 
 	register int		i;
 	register bool		vflag;
@@ -422,12 +492,12 @@ main(int argc, char *argv[])
 	cuthitime = absolute_max_time;
 #if HAVE_GETTEXT
 	setlocale(LC_ALL, "");
-#ifdef TZ_DOMAINDIR
+# ifdef TZ_DOMAINDIR
 	bindtextdomain(TZ_DOMAIN, TZ_DOMAINDIR);
-#endif /* defined TEXTDOMAINDIR */
+# endif /* defined TEXTDOMAINDIR */
 	textdomain(TZ_DOMAIN);
 #endif /* HAVE_GETTEXT */
-	progname = argv[0];
+	progname = argv[0] ? argv[0] : "zdump";
 	for (i = 1; i < argc; ++i)
 		if (strcmp(argv[i], "--version") == 0) {
 			printf("zdump %s%s\n", PKGVERSION, TZVERSION);
@@ -447,7 +517,7 @@ main(int argc, char *argv[])
 	  case -1:
 	    if (! (optind == argc - 1 && strcmp(argv[optind], "=") == 0))
 	      goto arg_processing_done;
-	    /* Fall through.  */
+	    ATTRIBUTE_FALLTHROUGH;
 	  default:
 	    usage(stderr, EXIT_FAILURE);
 	  }
@@ -484,8 +554,8 @@ main(int argc, char *argv[])
 			if (cuttimes != loend && !*loend) {
 				hi = lo;
 				if (hi < cuthitime) {
-					if (hi < absolute_min_time)
-						hi = absolute_min_time;
+					if (hi < absolute_min_time + 1)
+					  hi = absolute_min_time + 1;
 					cuthitime = hi;
 				}
 			} else if (cuttimes != loend && *loend == ','
@@ -497,8 +567,8 @@ main(int argc, char *argv[])
 					cutlotime = lo;
 				}
 				if (hi < cuthitime) {
-					if (hi < absolute_min_time)
-						hi = absolute_min_time;
+					if (hi < absolute_min_time + 1)
+					  hi = absolute_min_time + 1;
 					cuthitime = hi;
 				}
 			} else {
@@ -510,14 +580,17 @@ main(int argc, char *argv[])
 		}
 	}
 	gmtzinit();
-	INITIALIZE (now);
-	if (! (iflag | vflag | Vflag))
+	if (iflag | vflag | Vflag)
+	  now = 0;
+	else {
 	  now = time(NULL);
+	  now |= !now;
+	}
 	longest = 0;
 	for (i = optind; i < argc; i++) {
 	  size_t arglen = strlen(argv[i]);
 	  if (longest < arglen)
-	    longest = arglen < INT_MAX ? arglen : INT_MAX;
+	    longest = min(arglen, INT_MAX);
 	}
 
 	for (i = optind; i < argc; ++i) {
@@ -527,10 +600,12 @@ main(int argc, char *argv[])
 		struct tm tm, newtm;
 		bool tm_ok;
 		if (!tz) {
-		  perror(argv[i]);
+		  char const *e = strerror(errno);
+		  fprintf(stderr, _("%s: unknown timezone '%s': %s\n"),
+			  progname, argv[i], e);
 		  return EXIT_FAILURE;
 		}
-		if (! (iflag | vflag | Vflag)) {
+		if (now) {
 			show(tz, argv[i], now, false);
 			tzfree(tz);
 			continue;
@@ -539,12 +614,15 @@ main(int argc, char *argv[])
 		t = absolute_min_time;
 		if (! (iflag | Vflag)) {
 			show(tz, argv[i], t, true);
-			t += SECSPERDAY;
-			show(tz, argv[i], t, true);
+			if (my_localtime_rz(tz, &t, &tm) == NULL
+			    && t < cutlotime) {
+				time_t newt = cutlotime;
+				if (my_localtime_rz(tz, &newt, &newtm) != NULL)
+				  showextrema(tz, argv[i], t, NULL, newt);
+			}
 		}
-		if (t < cutlotime)
-			t = cutlotime;
-		INITIALIZE (ab);
+		if (t + 1 < cutlotime)
+		  t = cutlotime - 1;
 		tm_ok = my_localtime_rz(tz, &t, &tm) != NULL;
 		if (tm_ok) {
 		  ab = saveabbr(&abbrev, &abbrevsize, &tm);
@@ -552,19 +630,20 @@ main(int argc, char *argv[])
 		    showtrans("\nTZ=%f", &tm, t, ab, argv[i]);
 		    showtrans("-\t-\t%Q", &tm, t, ab, argv[i]);
 		  }
-		}
-		while (t < cuthitime) {
+		} else
+		  ab = NULL;
+		while (t < cuthitime - 1) {
 		  time_t newt = ((t < absolute_max_time - SECSPERDAY / 2
-				  && t + SECSPERDAY / 2 < cuthitime)
+				  && t + SECSPERDAY / 2 < cuthitime - 1)
 				 ? t + SECSPERDAY / 2
-				 : cuthitime);
+				 : cuthitime - 1);
 		  struct tm *newtmp = localtime_rz(tz, &newt, &newtm);
 		  bool newtm_ok = newtmp != NULL;
 		  if (tm_ok != newtm_ok
-		      || (tm_ok && (delta(&newtm, &tm) != newt - t
-				    || newtm.tm_isdst != tm.tm_isdst
-				    || strcmp(abbr(&newtm), ab) != 0))) {
-		    newt = hunt(tz, argv[i], t, newt);
+		      || (ab && (delta(&newtm, &tm) != newt - t
+				 || newtm.tm_isdst != tm.tm_isdst
+				 || strcmp(abbr(&newtm), ab) != 0))) {
+		    newt = hunt(tz, t, newt, false);
 		    newtmp = localtime_rz(tz, &newt, &newtm);
 		    newtm_ok = newtmp != NULL;
 		    if (iflag)
@@ -583,11 +662,15 @@ main(int argc, char *argv[])
 		  }
 		}
 		if (! (iflag | Vflag)) {
-			t = absolute_max_time;
-			t -= SECSPERDAY;
-			show(tz, argv[i], t, true);
-			t += SECSPERDAY;
-			show(tz, argv[i], t, true);
+			time_t newt = absolute_max_time;
+			t = cuthitime;
+			if (t < newt) {
+			  struct tm *tmp = my_localtime_rz(tz, &t, &tm);
+			  if (tmp != NULL
+			      && my_localtime_rz(tz, &newt, &newtm) == NULL)
+			    showextrema(tz, argv[i], t, tmp, newt);
+			}
+			show(tz, argv[i], absolute_max_time, true);
 		}
 		tzfree(tz);
 	}
@@ -640,36 +723,42 @@ yeartot(intmax_t y)
 	return t;
 }
 
+/* Search for a discontinuity in timezone TZ, in the
+   timestamps ranging from LOT through HIT.  LOT and HIT disagree
+   about some aspect of timezone.  If ONLY_OK, search only for
+   definedness changes, i.e., localtime succeeds on one side of the
+   transition but fails on the other side.  Return the timestamp just
+   before the transition from LOT's settings.  */
+
 static time_t
-hunt(timezone_t tz, char *name, time_t lot, time_t hit)
+hunt(timezone_t tz, time_t lot, time_t hit, bool only_ok)
 {
 	static char *		loab;
-	static size_t		loabsize;
-	char const *		ab;
-	time_t			t;
+	static ptrdiff_t	loabsize;
 	struct tm		lotm;
 	struct tm		tm;
+
+	/* Convert LOT into a broken-down time here, even though our
+	   caller already did that.  On platforms without TM_ZONE,
+	   tzname may have been altered since our caller broke down
+	   LOT, and tzname needs to be changed back.  */
 	bool lotm_ok = my_localtime_rz(tz, &lot, &lotm) != NULL;
 	bool tm_ok;
+	char const *ab = lotm_ok ? saveabbr(&loab, &loabsize, &lotm) : NULL;
 
-	if (lotm_ok)
-	  ab = saveabbr(&loab, &loabsize, &lotm);
 	for ( ; ; ) {
-		time_t diff = hit - lot;
-		if (diff < 2)
+		/* T = average of LOT and HIT, rounding down.
+		   Avoid overflow.  */
+		int rem_sum = lot % 2 + hit % 2;
+		time_t t = (rem_sum == 2) - (rem_sum < 0) + lot / 2 + hit / 2;
+		if (t == lot)
 			break;
-		t = lot;
-		t += diff / 2;
-		if (t <= lot)
-			++t;
-		else if (t >= hit)
-			--t;
 		tm_ok = my_localtime_rz(tz, &t, &tm) != NULL;
-		if (lotm_ok & tm_ok
-		    ? (delta(&tm, &lotm) == t - lot
-		       && tm.tm_isdst == lotm.tm_isdst
-		       && strcmp(abbr(&tm), ab) == 0)
-		    : lotm_ok == tm_ok) {
+		if (lotm_ok == tm_ok
+		    && (only_ok
+			|| (ab && tm.tm_isdst == lotm.tm_isdst
+			    && delta(&tm, &lotm) == t - lot
+			    && strcmp(abbr(&tm), ab) == 0))) {
 		  lot = t;
 		  if (tm_ok)
 		    lotm = tm;
@@ -685,11 +774,11 @@ hunt(timezone_t tz, char *name, time_t lot, time_t hit)
 static intmax_t
 delta_nonneg(struct tm *newp, struct tm *oldp)
 {
-	register intmax_t	result;
-	register int		tmy;
-
-	result = 0;
-	for (tmy = oldp->tm_year; tmy < newp->tm_year; ++tmy)
+	intmax_t oldy = oldp->tm_year;
+	int cycles = (newp->tm_year - oldy) / YEARSPERREPEAT;
+	intmax_t sec = SECSPERREPEAT, result = cycles * sec;
+	int tmy = oldp->tm_year + cycles * YEARSPERREPEAT;
+	for ( ; tmy < newp->tm_year; ++tmy)
 		result += DAYSPERNYEAR + isleap_sum(tmy, TM_YEAR_BASE);
 	result += newp->tm_yday - oldp->tm_yday;
 	result *= HOURSPERDAY;
@@ -730,7 +819,8 @@ adjusted_yday(struct tm const *a, struct tm const *b)
    my_gmtime_r and use its result instead of B.  Otherwise, B is the
    possibly nonnull result of an earlier call to my_gmtime_r.  */
 static long
-gmtoff(struct tm const *a, time_t *t, struct tm const *b)
+gmtoff(struct tm const *a, ATTRIBUTE_MAYBE_UNUSED time_t *t,
+       ATTRIBUTE_MAYBE_UNUSED struct tm const *b)
 {
 #ifdef TM_GMTOFF
   return a->TM_GMTOFF;
@@ -764,6 +854,7 @@ show(timezone_t tz, char *zone, time_t t, bool v)
 		gmtmp = my_gmtime_r(&t, &gmtm);
 		if (gmtmp == NULL) {
 			printf(tformat(), t);
+			printf(_(" (gmtime failed)"));
 		} else {
 			dumptime(gmtmp);
 			printf(" UT");
@@ -771,8 +862,11 @@ show(timezone_t tz, char *zone, time_t t, bool v)
 		printf(" = ");
 	}
 	tmp = my_localtime_rz(tz, &t, &tm);
-	dumptime(tmp);
-	if (tmp != NULL) {
+	if (tmp == NULL) {
+		printf(tformat(), t);
+		printf(_(" (localtime failed)"));
+	} else {
+		dumptime(tmp);
 		if (*abbr(tmp) != '\0')
 			printf(" %s", abbr(tmp));
 		if (v) {
@@ -787,13 +881,58 @@ show(timezone_t tz, char *zone, time_t t, bool v)
 		abbrok(abbr(tmp), zone);
 }
 
+/* Show timestamps just before and just after a transition between
+   defined and undefined (or vice versa) in either localtime or
+   gmtime.  These transitions are for timezone TZ with name ZONE, in
+   the range from LO (with broken-down time LOTMP if that is nonnull)
+   through HI.  LO and HI disagree on definedness.  */
+
+static void
+showextrema(timezone_t tz, char *zone, time_t lo, struct tm *lotmp, time_t hi)
+{
+  struct tm localtm[2], gmtm[2];
+  time_t t, boundary = hunt(tz, lo, hi, true);
+  bool old = false;
+  hi = (SECSPERDAY < hi - boundary
+	? boundary + SECSPERDAY
+	: hi + (hi < TIME_T_MAX));
+  if (SECSPERDAY < boundary - lo) {
+    lo = boundary - SECSPERDAY;
+    lotmp = my_localtime_rz(tz, &lo, &localtm[old]);
+  }
+  if (lotmp)
+    localtm[old] = *lotmp;
+  else
+    localtm[old].tm_sec = -1;
+  if (! my_gmtime_r(&lo, &gmtm[old]))
+    gmtm[old].tm_sec = -1;
+
+  /* Search sequentially for definedness transitions.  Although this
+     could be sped up by refining 'hunt' to search for either
+     localtime or gmtime definedness transitions, it hardly seems
+     worth the trouble.  */
+  for (t = lo + 1; t < hi; t++) {
+    bool new = !old;
+    if (! my_localtime_rz(tz, &t, &localtm[new]))
+      localtm[new].tm_sec = -1;
+    if (! my_gmtime_r(&t, &gmtm[new]))
+      gmtm[new].tm_sec = -1;
+    if (((localtm[old].tm_sec < 0) != (localtm[new].tm_sec < 0))
+	| ((gmtm[old].tm_sec < 0) != (gmtm[new].tm_sec < 0))) {
+      show(tz, zone, t - 1, true);
+      show(tz, zone, t, true);
+    }
+    old = new;
+  }
+}
+
 #if HAVE_SNPRINTF
 # define my_snprintf snprintf
 #else
 # include <stdarg.h>
 
 /* A substitute for snprintf that is good enough for zdump.  */
-static int ATTRIBUTE_FORMAT((printf, 3, 4))
+ATTRIBUTE_FORMAT((printf, 3, 4)) static int
 my_snprintf(char *s, size_t size, char const *format, ...)
 {
   int n;
@@ -831,7 +970,7 @@ my_snprintf(char *s, size_t size, char const *format, ...)
    fit, return the length that the string would have been if it had
    fit; do not overrun the output buffer.  */
 static int
-format_local_time(char *buf, size_t size, struct tm const *tm)
+format_local_time(char *buf, ptrdiff_t size, struct tm const *tm)
 {
   int ss = tm->tm_sec, mm = tm->tm_min, hh = tm->tm_hour;
   return (ss
@@ -854,7 +993,7 @@ format_local_time(char *buf, size_t size, struct tm const *tm)
    the length that the string would have been if it had fit; do not
    overrun the output buffer.  */
 static int
-format_utc_offset(char *buf, size_t size, struct tm const *tm, time_t t)
+format_utc_offset(char *buf, ptrdiff_t size, struct tm const *tm, time_t t)
 {
   long off = gmtoff(tm, &t, NULL);
   char sign = ((off < 0
@@ -883,11 +1022,11 @@ format_utc_offset(char *buf, size_t size, struct tm const *tm, time_t t)
    If the representation's length is less than SIZE, return the
    length; the representation is not null terminated.  Otherwise
    return SIZE, to indicate that BUF is too small.  */
-static size_t
-format_quoted_string(char *buf, size_t size, char const *p)
+static ptrdiff_t
+format_quoted_string(char *buf, ptrdiff_t size, char const *p)
 {
   char *b = buf;
-  size_t s = size;
+  ptrdiff_t s = size;
   if (!s)
     return size;
   *b++ = '"', s--;
@@ -925,11 +1064,11 @@ format_quoted_string(char *buf, size_t size, char const *p)
       and omit any trailing tabs.  */
 
 static bool
-istrftime(char *buf, size_t size, char const *time_fmt,
+istrftime(char *buf, ptrdiff_t size, char const *time_fmt,
 	  struct tm const *tm, time_t t, char const *ab, char const *zone_name)
 {
   char *b = buf;
-  size_t s = size;
+  ptrdiff_t s = size;
   char const *f = time_fmt, *p;
 
   for (p = f; ; p++)
@@ -938,9 +1077,9 @@ istrftime(char *buf, size_t size, char const *time_fmt,
     else if (!*p
 	     || (*p == '%'
 		 && (p[1] == 'f' || p[1] == 'L' || p[1] == 'Q'))) {
-      size_t formatted_len;
-      size_t f_prefix_len = p - f;
-      size_t f_prefix_copy_size = p - f + 2;
+      ptrdiff_t formatted_len;
+      ptrdiff_t f_prefix_len = p - f;
+      ptrdiff_t f_prefix_copy_size = sumsize(f_prefix_len, 2);
       char fbuf[100];
       bool oversized = sizeof fbuf <= f_prefix_copy_size;
       char *f_prefix_copy = oversized ? xmalloc(f_prefix_copy_size) : fbuf;
@@ -972,7 +1111,7 @@ istrftime(char *buf, size_t size, char const *time_fmt,
 	  b += offlen, s -= offlen;
 	  if (show_abbr) {
 	    char const *abp;
-	    size_t len;
+	    ptrdiff_t len;
 	    if (s <= 1)
 	      return false;
 	    *b++ = '\t', s--;
@@ -1011,7 +1150,7 @@ showtrans(char const *time_fmt, struct tm const *tm, time_t t, char const *ab,
     putchar('\n');
   } else {
     char stackbuf[1000];
-    size_t size = sizeof stackbuf;
+    ptrdiff_t size = sizeof stackbuf;
     char *buf = stackbuf;
     char *bufalloc = NULL;
     while (! istrftime(buf, size, time_fmt, tm, t, ab, zone_name)) {
@@ -1040,28 +1179,45 @@ abbr(struct tm const *tmp)
 
 /*
 ** The code below can fail on certain theoretical systems;
-** it works on all known real-world systems as of 2004-12-30.
+** it works on all known real-world systems as of 2022-01-25.
 */
 
 static const char *
 tformat(void)
 {
+#if HAVE__GENERIC
+	/* C11-style _Generic is more likely to return the correct
+	   format when distinct types have the same size.  */
+	char const *fmt =
+	  _Generic(+ (time_t) 0,
+		   int: "%d", long: "%ld", long long: "%lld",
+		   unsigned: "%u", unsigned long: "%lu",
+		   unsigned long long: "%llu",
+		   default: NULL);
+	if (fmt)
+	  return fmt;
+	fmt = _Generic((time_t) 0,
+		       intmax_t: "%"PRIdMAX, uintmax_t: "%"PRIuMAX,
+		       default: NULL);
+	if (fmt)
+	  return fmt;
+#endif
 	if (0 > (time_t) -1) {		/* signed */
-		if (sizeof (time_t) == sizeof (intmax_t))
+		if (sizeof(time_t) == sizeof(intmax_t))
 			return "%"PRIdMAX;
-		if (sizeof (time_t) > sizeof (long))
+		if (sizeof(time_t) > sizeof(long))
 			return "%lld";
-		if (sizeof (time_t) > sizeof (int))
+		if (sizeof(time_t) > sizeof(int))
 			return "%ld";
 		return "%d";
 	}
 #ifdef PRIuMAX
-	if (sizeof (time_t) == sizeof (uintmax_t))
+	if (sizeof(time_t) == sizeof(uintmax_t))
 		return "%"PRIuMAX;
 #endif
-	if (sizeof (time_t) > sizeof (unsigned long))
+	if (sizeof(time_t) > sizeof(unsigned long))
 		return "%llu";
-	if (sizeof (time_t) > sizeof (unsigned int))
+	if (sizeof(time_t) > sizeof(unsigned int))
 		return "%lu";
 	return "%u";
 }
@@ -1076,33 +1232,24 @@ dumptime(register const struct tm *timeptr)
 		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
 		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
 	};
-	register const char *	wn;
-	register const char *	mn;
 	register int		lead;
 	register int		trail;
+	int DIVISOR = 10;
 
-	if (timeptr == NULL) {
-		printf("NULL");
-		return;
-	}
 	/*
 	** The packaged localtime_rz and gmtime_r never put out-of-range
 	** values in tm_wday or tm_mon, but since this code might be compiled
 	** with other (perhaps experimental) versions, paranoia is in order.
 	*/
-	if (timeptr->tm_wday < 0 || timeptr->tm_wday >=
-		(int) (sizeof wday_name / sizeof wday_name[0]))
-			wn = "???";
-	else		wn = wday_name[timeptr->tm_wday];
-	if (timeptr->tm_mon < 0 || timeptr->tm_mon >=
-		(int) (sizeof mon_name / sizeof mon_name[0]))
-			mn = "???";
-	else		mn = mon_name[timeptr->tm_mon];
 	printf("%s %s%3d %.2d:%.2d:%.2d ",
-		wn, mn,
+		((0 <= timeptr->tm_wday
+		  && timeptr->tm_wday < sizeof wday_name / sizeof wday_name[0])
+		 ? wday_name[timeptr->tm_wday] : "???"),
+		((0 <= timeptr->tm_mon
+		  && timeptr->tm_mon < sizeof mon_name / sizeof mon_name[0])
+		 ? mon_name[timeptr->tm_mon] : "???"),
 		timeptr->tm_mday, timeptr->tm_hour,
 		timeptr->tm_min, timeptr->tm_sec);
-#define DIVISOR	10
 	trail = timeptr->tm_year % DIVISOR + TM_YEAR_BASE % DIVISOR;
 	lead = timeptr->tm_year / DIVISOR + TM_YEAR_BASE / DIVISOR +
 		trail / DIVISOR;