xref: /freebsd/contrib/tzcode/zdump.c (revision d15f2551b25f79ddcbe289faa95e655100b952da)
1 /* Dump time zone data in a textual format.  */
2 
3 /*
4 ** This file is in the public domain, so clarified as of
5 ** 2009-05-17 by Arthur David Olson.
6 */
7 
8 #include "version.h"
9 
10 #ifndef NETBSD_INSPIRED
11 # define NETBSD_INSPIRED 1
12 #endif
13 
14 #include "private.h"
15 #include <stdio.h>
16 
17 #ifndef HAVE_LOCALTIME_R
18 # define HAVE_LOCALTIME_R 1
19 #endif
20 
21 #ifndef HAVE_LOCALTIME_RZ
22 # ifdef TM_ZONE
23 #  define HAVE_LOCALTIME_RZ (NETBSD_INSPIRED && USE_LTZ)
24 # else
25 #  define HAVE_LOCALTIME_RZ 0
26 # endif
27 #endif
28 
29 #ifndef HAVE_TZSET
30 # define HAVE_TZSET 1
31 #endif
32 
33 #ifndef ZDUMP_LO_YEAR
34 # define ZDUMP_LO_YEAR (-500)
35 #endif /* !defined ZDUMP_LO_YEAR */
36 
37 #ifndef ZDUMP_HI_YEAR
38 # define ZDUMP_HI_YEAR 2500
39 #endif /* !defined ZDUMP_HI_YEAR */
40 
41 #define SECSPERNYEAR	(SECSPERDAY * DAYSPERNYEAR)
42 #define SECSPERLYEAR	(SECSPERNYEAR + SECSPERDAY)
43 #define SECSPER400YEARS	(SECSPERNYEAR * (intmax_t) (300 + 3)	\
44 			 + SECSPERLYEAR * (intmax_t) (100 - 3))
45 
46 /*
47 ** True if SECSPER400YEARS is known to be representable as an
48 ** intmax_t.  It's OK that SECSPER400YEARS_FITS can in theory be false
49 ** even if SECSPER400YEARS is representable, because when that happens
50 ** the code merely runs a bit more slowly, and this slowness doesn't
51 ** occur on any practical platform.
52 */
53 enum { SECSPER400YEARS_FITS = SECSPERLYEAR <= INTMAX_MAX / 400 };
54 
55 #if HAVE_GETTEXT
56 # include <locale.h> /* for setlocale */
57 #endif /* HAVE_GETTEXT */
58 
59 #if ! HAVE_LOCALTIME_RZ
60 # undef  timezone_t
61 # define timezone_t char **
62 #endif
63 
64 /* The minimum and maximum finite time values.  */
65 enum { atime_shift = CHAR_BIT * sizeof(time_t) - 2 };
66 static time_t const absolute_min_time =
67   ((time_t) -1 < 0
68    ? (- ((time_t) ~ (time_t) 0 < 0)
69       - (((time_t) 1 << atime_shift) - 1 + ((time_t) 1 << atime_shift)))
70    : 0);
71 static time_t const absolute_max_time =
72   ((time_t) -1 < 0
73    ? (((time_t) 1 << atime_shift) - 1 + ((time_t) 1 << atime_shift))
74    : -1);
75 static size_t	longest;
76 static char const *progname;
77 static bool	warned;
78 static bool	errout;
79 
80 static char const *abbr(struct tm const *);
81 static intmax_t delta(struct tm *, struct tm *);
82 static void dumptime(struct tm const *);
83 static time_t hunt(timezone_t, time_t, time_t, bool);
84 static void show(timezone_t, char *, time_t, bool);
85 static void showextrema(timezone_t, char *, time_t, struct tm *, time_t);
86 static void showtrans(char const *, struct tm const *, time_t, char const *,
87 		      char const *);
88 static const char *tformat(void);
89 ATTRIBUTE_PURE_114833 static time_t yeartot(intmax_t);
90 
91 /* Is C an ASCII digit?  */
92 static bool
93 is_digit(char c)
94 {
95   return '0' <= c && c <= '9';
96 }
97 
98 /* Is A an alphabetic character in the C locale?  */
99 static bool
100 is_alpha(char a)
101 {
102 	switch (a) {
103 	  default:
104 		return false;
105 	  case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G':
106 	  case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N':
107 	  case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U':
108 	  case 'V': case 'W': case 'X': case 'Y': case 'Z':
109 	  case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g':
110 	  case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n':
111 	  case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u':
112 	  case 'v': case 'w': case 'x': case 'y': case 'z':
113 		return true;
114 	}
115 }
116 
117 ATTRIBUTE_NORETURN static void
118 size_overflow(void)
119 {
120   fprintf(stderr, _("%s: size overflow\n"), progname);
121   exit(EXIT_FAILURE);
122 }
123 
124 /* Return A + B, exiting if the result would overflow either ptrdiff_t
125    or size_t.  A and B are both nonnegative.  */
126 ATTRIBUTE_PURE_114833_HACK
127 static ptrdiff_t
128 sumsize(ptrdiff_t a, ptrdiff_t b)
129 {
130 #ifdef ckd_add
131   ptrdiff_t sum;
132   if (!ckd_add(&sum, a, b) && sum <= INDEX_MAX)
133     return sum;
134 #else
135   if (a <= INDEX_MAX && b <= INDEX_MAX - a)
136     return a + b;
137 #endif
138   size_overflow();
139 }
140 
141 
142 /* Return a pointer to a newly allocated buffer of size SIZE, exiting
143    on failure.  SIZE should be positive.  */
144 static void *
145 xmalloc(ptrdiff_t size)
146 {
147   void *p = malloc(size);
148   if (!p) {
149     fprintf(stderr, _("%s: Memory exhausted\n"), progname);
150     exit(EXIT_FAILURE);
151   }
152   return p;
153 }
154 
155 #if ! HAVE_TZSET
156 # undef tzset
157 # define tzset zdump_tzset
158 static void tzset(void) { }
159 #endif
160 
161 /* Assume gmtime_r works if localtime_r does.
162    A replacement localtime_r is defined below if needed.  */
163 #if ! HAVE_LOCALTIME_R
164 
165 # undef gmtime_r
166 # define gmtime_r zdump_gmtime_r
167 
168 static struct tm *
169 gmtime_r(time_t *tp, struct tm *tmp)
170 {
171   struct tm *r = gmtime(tp);
172   if (r) {
173     *tmp = *r;
174     r = tmp;
175   }
176   return r;
177 }
178 
179 #endif
180 
181 /* Platforms with TM_ZONE don't need tzname, so they can use the
182    faster localtime_rz or localtime_r if available.  */
183 
184 #if defined TM_ZONE && HAVE_LOCALTIME_RZ
185 # define USE_LOCALTIME_RZ true
186 #else
187 # define USE_LOCALTIME_RZ false
188 #endif
189 
190 #if ! USE_LOCALTIME_RZ
191 
192 # if !defined TM_ZONE || ! HAVE_LOCALTIME_R || ! HAVE_TZSET
193 #  undef localtime_r
194 #  define localtime_r zdump_localtime_r
195 static struct tm *
196 localtime_r(time_t *tp, struct tm *tmp)
197 {
198   struct tm *r = localtime(tp);
199   if (r) {
200     *tmp = *r;
201     r = tmp;
202   }
203   return r;
204 }
205 # endif
206 
207 # undef localtime_rz
208 # define localtime_rz zdump_localtime_rz
209 static struct tm *
210 localtime_rz(ATTRIBUTE_MAYBE_UNUSED timezone_t rz, time_t *tp, struct tm *tmp)
211 {
212   return localtime_r(tp, tmp);
213 }
214 
215 # undef tzalloc
216 # undef tzfree
217 # define tzalloc zdump_tzalloc
218 # define tzfree zdump_tzfree
219 
220 static timezone_t
221 tzalloc(char const *val)
222 {
223 # if HAVE_SETENV
224   if (setenv("TZ", val, 1) != 0) {
225     char const *e = strerror(errno);
226     fprintf(stderr, _("%s: setenv: %s\n"), progname, e);
227     exit(EXIT_FAILURE);
228   }
229   tzset();
230   return &optarg;  /* Any valid non-null char ** will do.  */
231 # else
232   enum { TZeqlen = 3 };
233   static char const TZeq[TZeqlen] = "TZ=";
234   static char **fakeenv;
235   static ptrdiff_t fakeenv0size;
236   void *freeable = NULL;
237   char **env = fakeenv, **initial_environ;
238   ptrdiff_t valsize = strlen(val) + 1;
239   if (fakeenv0size < valsize) {
240     char **e = environ, **to;
241     ptrdiff_t initial_nenvptrs = 1;  /* Counting the trailing NULL pointer.  */
242 
243     while (*e++) {
244 #  ifdef ckd_add
245       if (ckd_add(&initial_nenvptrs, initial_nenvptrs, 1)
246 	  || INDEX_MAX < initial_nenvptrs)
247 	size_overflow();
248 #  else
249       if (initial_nenvptrs == INDEX_MAX / sizeof *environ)
250 	size_overflow();
251       initial_nenvptrs++;
252 #  endif
253     }
254     fakeenv0size = sumsize(valsize, valsize);
255     fakeenv0size = max(fakeenv0size, 64);
256     freeable = env;
257     fakeenv = env =
258       xmalloc(sumsize(sumsize(sizeof *environ,
259 			      initial_nenvptrs * sizeof *environ),
260 		      sumsize(TZeqlen, fakeenv0size)));
261     to = env + 1;
262     for (e = environ; (*to = *e); e++)
263       to += strncmp(*e, TZeq, TZeqlen) != 0;
264     env[0] = memcpy(to + 1, TZeq, TZeqlen);
265   }
266   memcpy(env[0] + TZeqlen, val, valsize);
267   initial_environ = environ;
268   environ = env;
269   tzset();
270   free(freeable);
271   return initial_environ;
272 # endif
273 }
274 
275 static void
276 tzfree(ATTRIBUTE_MAYBE_UNUSED timezone_t initial_environ)
277 {
278 # if !HAVE_SETENV
279   environ = initial_environ;
280   tzset();
281 # else
282   (void)initial_environ;
283 # endif
284 }
285 #endif /* ! USE_LOCALTIME_RZ */
286 
287 /* A UT time zone, and its initializer.  */
288 static timezone_t gmtz;
289 static void
290 gmtzinit(void)
291 {
292   if (USE_LOCALTIME_RZ) {
293     /* Try "GMT" first to find out whether this is one of the rare
294        platforms where time_t counts leap seconds; this works due to
295        the "Zone GMT 0 - GMT" line in the "etcetera" file.  If "GMT"
296        fails, fall back on "GMT0" which might be similar due to the
297        "Link GMT GMT0" line in the "backward" file, and which
298        should work on all POSIX platforms.  The rest of zdump does not
299        use the "GMT" abbreviation that comes from this setting, so it
300        is OK to use "GMT" here rather than the modern "UTC" which
301        would not work on platforms that omit the "backward" file.  */
302     gmtz = tzalloc("GMT");
303     if (!gmtz) {
304       static char const gmt0[] = "GMT0";
305       gmtz = tzalloc(gmt0);
306       if (!gmtz) {
307 	char const *e = strerror(errno);
308 	fprintf(stderr, _("%s: unknown timezone '%s': %s\n"),
309 		progname, gmt0, e);
310 	exit(EXIT_FAILURE);
311       }
312     }
313   }
314 }
315 
316 /* Convert *TP to UT, storing the broken-down time into *TMP.
317    Return TMP if successful, NULL otherwise.  This is like gmtime_r(TP, TMP),
318    except typically faster if USE_LOCALTIME_RZ.  */
319 static struct tm *
320 my_gmtime_r(time_t *tp, struct tm *tmp)
321 {
322   return USE_LOCALTIME_RZ ? localtime_rz(gmtz, tp, tmp) : gmtime_r(tp, tmp);
323 }
324 
325 static void
326 abbrok(const char *const abbrp, const char *const zone)
327 {
328 	register const char *	cp;
329 	register const char *	wp;
330 
331 	if (warned)
332 		return;
333 	cp = abbrp;
334 	while (is_alpha(*cp) || is_digit(*cp) || *cp == '-' || *cp == '+')
335 		++cp;
336 	if (*cp)
337 	  wp = _("has characters other than ASCII alphanumerics, '-' or '+'");
338 	else if (cp - abbrp < 3)
339 	  wp = _("has fewer than 3 characters");
340 	else if (cp - abbrp > 6)
341 	  wp = _("has more than 6 characters");
342 	else
343 	  return;
344 	fflush(stdout);
345 	fprintf(stderr,
346 		_("%s: warning: zone \"%s\" abbreviation \"%s\" %s\n"),
347 		progname, zone, abbrp, wp);
348 	warned = errout = true;
349 }
350 
351 /* Return a time zone abbreviation.  If the abbreviation needs to be
352    saved, use *BUF (of size *BUFALLOC) to save it, and return the
353    abbreviation in the possibly reallocated *BUF.  Otherwise, just
354    return the abbreviation.  Get the abbreviation from TMP.
355    Exit on memory allocation failure.  */
356 static char const *
357 saveabbr(char **buf, ptrdiff_t *bufalloc, struct tm const *tmp)
358 {
359   char const *ab = abbr(tmp);
360   if (HAVE_LOCALTIME_RZ)
361     return ab;
362   else {
363     ptrdiff_t absize = strlen(ab) + 1;
364     if (*bufalloc < absize) {
365       free(*buf);
366 
367       /* Make the new buffer at least twice as long as the old,
368 	 to avoid O(N**2) behavior on repeated calls.  */
369       *bufalloc = sumsize(*bufalloc, absize);
370 
371       *buf = xmalloc(*bufalloc);
372     }
373     return strcpy(*buf, ab);
374   }
375 }
376 
377 static void
378 close_file(FILE *stream)
379 {
380   char const *e = (ferror(stream) ? _("I/O error")
381 		   : fclose(stream) != 0 ? strerror(errno) : NULL);
382   if (e) {
383     fprintf(stderr, "%s: %s\n", progname, e);
384     exit(EXIT_FAILURE);
385   }
386 }
387 
388 static void
389 usage(FILE * const stream, const int status)
390 {
391 	fprintf(stream,
392 _("%s: usage: %s OPTIONS TIMEZONE ...\n"
393   "Options include:\n"
394   "  -c [L,]U   Start at year L (default -500), end before year U (default 2500)\n"
395   "  -t [L,]U   Start at time L, end before time U (in seconds since 1970)\n"
396   "  -i         List transitions briefly (format is experimental)\n" \
397   "  -v         List transitions verbosely\n"
398   "  -V         List transitions a bit less verbosely\n"
399   "  --help     Output this help\n"
400   "  --version  Output version info\n"
401   "\n"
402   "Report bugs to %s.\n"),
403 		progname, progname, REPORT_BUGS_TO);
404 	if (status == EXIT_SUCCESS)
405 	  close_file(stream);
406 	exit(status);
407 }
408 
409 int
410 main(int argc, char *argv[])
411 {
412 	/* These are static so that they're initially zero.  */
413 	static char *		abbrev;
414 	static ptrdiff_t	abbrevsize;
415 
416 	register int		i;
417 	register bool		vflag;
418 	register bool		Vflag;
419 	register char *		cutarg;
420 	register char *		cuttimes;
421 	register time_t		cutlotime;
422 	register time_t		cuthitime;
423 	time_t			now;
424 	bool iflag = false;
425 	size_t arglenmax = 0;
426 
427 	cutlotime = absolute_min_time;
428 	cuthitime = absolute_max_time;
429 #if HAVE_GETTEXT
430 	setlocale(LC_ALL, "");
431 # ifdef TZ_DOMAINDIR
432 	bindtextdomain(TZ_DOMAIN, TZ_DOMAINDIR);
433 # endif /* defined TEXTDOMAINDIR */
434 	textdomain(TZ_DOMAIN);
435 #endif /* HAVE_GETTEXT */
436 	progname = argv[0] ? argv[0] : "zdump";
437 	for (i = 1; i < argc; ++i)
438 		if (strcmp(argv[i], "--version") == 0) {
439 			printf("zdump %s%s\n", PKGVERSION, TZVERSION);
440 			return EXIT_SUCCESS;
441 		} else if (strcmp(argv[i], "--help") == 0) {
442 			usage(stdout, EXIT_SUCCESS);
443 		}
444 	vflag = Vflag = false;
445 	cutarg = cuttimes = NULL;
446 	for (;;)
447 	  switch (getopt(argc, argv, "c:it:vV")) {
448 	  case 'c': cutarg = optarg; break;
449 	  case 't': cuttimes = optarg; break;
450 	  case 'i': iflag = true; break;
451 	  case 'v': vflag = true; break;
452 	  case 'V': Vflag = true; break;
453 	  case -1:
454 	    if (! (optind == argc - 1 && strcmp(argv[optind], "=") == 0))
455 	      goto arg_processing_done;
456 	    ATTRIBUTE_FALLTHROUGH;
457 	  default:
458 	    usage(stderr, EXIT_FAILURE);
459 	  }
460  arg_processing_done:;
461 
462 	if (iflag | vflag | Vflag) {
463 		intmax_t	lo;
464 		intmax_t	hi;
465 		char *loend, *hiend;
466 		register intmax_t cutloyear = ZDUMP_LO_YEAR;
467 		register intmax_t cuthiyear = ZDUMP_HI_YEAR;
468 		if (cutarg != NULL) {
469 			lo = strtoimax(cutarg, &loend, 10);
470 			if (cutarg != loend && !*loend) {
471 				hi = lo;
472 				cuthiyear = hi;
473 			} else if (cutarg != loend && *loend == ','
474 				   && (hi = strtoimax(loend + 1, &hiend, 10),
475 				       loend + 1 != hiend && !*hiend)) {
476 				cutloyear = lo;
477 				cuthiyear = hi;
478 			} else {
479 				fprintf(stderr, _("%s: wild -c argument %s\n"),
480 					progname, cutarg);
481 				return EXIT_FAILURE;
482 			}
483 		}
484 		if (cutarg != NULL || cuttimes == NULL) {
485 			cutlotime = yeartot(cutloyear);
486 			cuthitime = yeartot(cuthiyear);
487 		}
488 		if (cuttimes != NULL) {
489 			lo = strtoimax(cuttimes, &loend, 10);
490 			if (cuttimes != loend && !*loend) {
491 				hi = lo;
492 				if (hi < cuthitime) {
493 					if (hi < absolute_min_time + 1)
494 					  hi = absolute_min_time + 1;
495 					cuthitime = hi;
496 				}
497 			} else if (cuttimes != loend && *loend == ','
498 				   && (hi = strtoimax(loend + 1, &hiend, 10),
499 				       loend + 1 != hiend && !*hiend)) {
500 				if (cutlotime < lo) {
501 					if (absolute_max_time < lo)
502 						lo = absolute_max_time;
503 					cutlotime = lo;
504 				}
505 				if (hi < cuthitime) {
506 					if (hi < absolute_min_time + 1)
507 					  hi = absolute_min_time + 1;
508 					cuthitime = hi;
509 				}
510 			} else {
511 				fprintf(stderr,
512 					_("%s: wild -t argument %s\n"),
513 					progname, cuttimes);
514 				return EXIT_FAILURE;
515 			}
516 		}
517 	}
518 	gmtzinit();
519 	if (iflag | vflag | Vflag)
520 	  now = 0;
521 	else {
522 	  now = time(NULL);
523 	  now |= !now;
524 	}
525 	for (i = optind; i < argc; i++) {
526 	  size_t arglen = strlen(argv[i]);
527 	  if (arglenmax < arglen)
528 	    arglenmax = arglen;
529 	}
530 	if (!HAVE_SETENV && INDEX_MAX <= arglenmax)
531 	  size_overflow();
532 	longest = min(arglenmax, INT_MAX - 2);
533 
534 	for (i = optind; i < argc; ++i) {
535 		/* Treat "-" as standard input on platforms with /dev/stdin.
536 		   It's not worth the bother of supporting "-" on other
537 		   platforms, as that would need temp files.  */
538 		timezone_t tz = tzalloc(strcmp(argv[i], "-") == 0
539 					? "/dev/stdin" : argv[i]);
540 		char const *ab;
541 		time_t t;
542 		struct tm tm, newtm;
543 		bool tm_ok;
544 		if (!tz) {
545 		  char const *e = strerror(errno);
546 		  fprintf(stderr, _("%s: unknown timezone '%s': %s\n"),
547 			  progname, argv[i], e);
548 		  return EXIT_FAILURE;
549 		}
550 		if (now) {
551 			show(tz, argv[i], now, false);
552 			tzfree(tz);
553 			continue;
554 		}
555 		warned = false;
556 		t = absolute_min_time;
557 		if (! (iflag | Vflag)) {
558 			show(tz, argv[i], t, true);
559 			if (localtime_rz(tz, &t, &tm) == NULL
560 			    && t < cutlotime) {
561 				time_t newt = cutlotime;
562 				if (localtime_rz(tz, &newt, &newtm) != NULL)
563 				  showextrema(tz, argv[i], t, NULL, newt);
564 			}
565 		}
566 		if (t + 1 < cutlotime)
567 		  t = cutlotime - 1;
568 		tm_ok = localtime_rz(tz, &t, &tm) != NULL;
569 		if (tm_ok) {
570 		  ab = saveabbr(&abbrev, &abbrevsize, &tm);
571 		  if (iflag) {
572 		    showtrans("\nTZ=%f", &tm, t, ab, argv[i]);
573 		    showtrans("-\t-\t%Q", &tm, t, ab, argv[i]);
574 		  }
575 		} else
576 		  ab = NULL;
577 		while (t < cuthitime - 1) {
578 		  time_t newt = ((t < absolute_max_time - SECSPERDAY / 2
579 				  && t + SECSPERDAY / 2 < cuthitime - 1)
580 				 ? t + SECSPERDAY / 2
581 				 : cuthitime - 1);
582 		  struct tm *newtmp = localtime_rz(tz, &newt, &newtm);
583 		  bool newtm_ok = newtmp != NULL;
584 		  if (tm_ok != newtm_ok
585 		      || (ab && (delta(&newtm, &tm) != newt - t
586 				 || newtm.tm_isdst != tm.tm_isdst
587 				 || strcmp(abbr(&newtm), ab) != 0))) {
588 		    newt = hunt(tz, t, newt, false);
589 		    newtmp = localtime_rz(tz, &newt, &newtm);
590 		    newtm_ok = newtmp != NULL;
591 		    if (iflag)
592 		      showtrans("%Y-%m-%d\t%L\t%Q", newtmp, newt,
593 				newtm_ok ? abbr(&newtm) : NULL, argv[i]);
594 		    else {
595 		      show(tz, argv[i], newt - 1, true);
596 		      show(tz, argv[i], newt, true);
597 		    }
598 		  }
599 		  t = newt;
600 		  tm_ok = newtm_ok;
601 		  if (newtm_ok) {
602 		    ab = saveabbr(&abbrev, &abbrevsize, &newtm);
603 		    tm = newtm;
604 		  }
605 		}
606 		if (! (iflag | Vflag)) {
607 			time_t newt = absolute_max_time;
608 			t = cuthitime;
609 			if (t < newt) {
610 			  struct tm *tmp = localtime_rz(tz, &t, &tm);
611 			  if (tmp != NULL
612 			      && localtime_rz(tz, &newt, &newtm) == NULL)
613 			    showextrema(tz, argv[i], t, tmp, newt);
614 			}
615 			show(tz, argv[i], absolute_max_time, true);
616 		}
617 		tzfree(tz);
618 	}
619 	close_file(stdout);
620 	if (errout && (ferror(stderr) || fclose(stderr) != 0))
621 	  return EXIT_FAILURE;
622 	return EXIT_SUCCESS;
623 }
624 
625 static time_t
626 yeartot(intmax_t y)
627 {
628 	register intmax_t	myy, seconds, years;
629 	register time_t		t;
630 
631 	myy = EPOCH_YEAR;
632 	t = 0;
633 	while (myy < y) {
634 		if (SECSPER400YEARS_FITS && 400 <= y - myy) {
635 			intmax_t diff400 = (y - myy) / 400;
636 			if (INTMAX_MAX / SECSPER400YEARS < diff400)
637 				return absolute_max_time;
638 			seconds = diff400 * SECSPER400YEARS;
639 			years = diff400 * 400;
640 		} else {
641 			seconds = isleap(myy) ? SECSPERLYEAR : SECSPERNYEAR;
642 			years = 1;
643 		}
644 		myy += years;
645 		if (t > absolute_max_time - seconds)
646 			return absolute_max_time;
647 		t += seconds;
648 	}
649 	while (y < myy) {
650 		if (SECSPER400YEARS_FITS && y + 400 <= myy && myy < 0) {
651 			intmax_t diff400 = (myy - y) / 400;
652 			if (INTMAX_MAX / SECSPER400YEARS < diff400)
653 				return absolute_min_time;
654 			seconds = diff400 * SECSPER400YEARS;
655 			years = diff400 * 400;
656 		} else {
657 			seconds = isleap(myy - 1) ? SECSPERLYEAR : SECSPERNYEAR;
658 			years = 1;
659 		}
660 		myy -= years;
661 		if (t < absolute_min_time + seconds)
662 			return absolute_min_time;
663 		t -= seconds;
664 	}
665 	return t;
666 }
667 
668 /* Search for a discontinuity in timezone TZ, in the
669    timestamps ranging from LOT through HIT.  LOT and HIT disagree
670    about some aspect of timezone.  If ONLY_OK, search only for
671    definedness changes, i.e., localtime succeeds on one side of the
672    transition but fails on the other side.  Return the timestamp just
673    before the transition from LOT's settings.  */
674 
675 static time_t
676 hunt(timezone_t tz, time_t lot, time_t hit, bool only_ok)
677 {
678 	static char *		loab;
679 	static ptrdiff_t	loabsize;
680 	struct tm		lotm;
681 	struct tm		tm;
682 
683 	/* Convert LOT into a broken-down time here, even though our
684 	   caller already did that.  On platforms without TM_ZONE,
685 	   tzname may have been altered since our caller broke down
686 	   LOT, and tzname needs to be changed back.  */
687 	bool lotm_ok = localtime_rz(tz, &lot, &lotm) != NULL;
688 	bool tm_ok;
689 	char const *ab = lotm_ok ? saveabbr(&loab, &loabsize, &lotm) : NULL;
690 
691 	for ( ; ; ) {
692 		/* T = average of LOT and HIT, rounding down.
693 		   Avoid overflow.  */
694 		int rem_sum = lot % 2 + hit % 2;
695 		time_t t = (rem_sum == 2) - (rem_sum < 0) + lot / 2 + hit / 2;
696 		if (t == lot)
697 			break;
698 		tm_ok = localtime_rz(tz, &t, &tm) != NULL;
699 		if (lotm_ok == tm_ok
700 		    && (only_ok
701 			|| (ab && tm.tm_isdst == lotm.tm_isdst
702 			    && delta(&tm, &lotm) == t - lot
703 			    && strcmp(abbr(&tm), ab) == 0))) {
704 		  lot = t;
705 		  if (tm_ok)
706 		    lotm = tm;
707 		} else	hit = t;
708 	}
709 	return hit;
710 }
711 
712 /*
713 ** Thanks to Paul Eggert for logic used in delta_nonneg.
714 */
715 
716 static intmax_t
717 delta_nonneg(struct tm *newp, struct tm *oldp)
718 {
719 	intmax_t oldy = oldp->tm_year;
720 	int cycles = (newp->tm_year - oldy) / YEARSPERREPEAT;
721 	intmax_t sec = SECSPERREPEAT, result = cycles * sec;
722 	int tmy = oldp->tm_year + cycles * YEARSPERREPEAT;
723 	for ( ; tmy < newp->tm_year; ++tmy)
724 		result += DAYSPERNYEAR + isleap_sum(tmy, TM_YEAR_BASE);
725 	result += newp->tm_yday - oldp->tm_yday;
726 	result *= HOURSPERDAY;
727 	result += newp->tm_hour - oldp->tm_hour;
728 	result *= MINSPERHOUR;
729 	result += newp->tm_min - oldp->tm_min;
730 	result *= SECSPERMIN;
731 	result += newp->tm_sec - oldp->tm_sec;
732 	return result;
733 }
734 
735 static intmax_t
736 delta(struct tm *newp, struct tm *oldp)
737 {
738   return (newp->tm_year < oldp->tm_year
739 	  ? -delta_nonneg(oldp, newp)
740 	  : delta_nonneg(newp, oldp));
741 }
742 
743 #ifndef TM_GMTOFF
744 /* Return A->tm_yday, adjusted to compare it fairly to B->tm_yday.
745    Assume A and B differ by at most one year.  */
746 static int
747 adjusted_yday(struct tm const *a, struct tm const *b)
748 {
749   int yday = a->tm_yday;
750   if (b->tm_year < a->tm_year)
751     yday += 365 + isleap_sum(b->tm_year, TM_YEAR_BASE);
752   return yday;
753 }
754 #endif
755 
756 /* If A is the broken-down local time and B the broken-down UT for
757    the same instant, return A's UT offset in seconds, where positive
758    offsets are east of Greenwich.  On failure, return LONG_MIN.
759 
760    If T is nonnull, *T is the timestamp that corresponds to A; call
761    my_gmtime_r and use its result instead of B.  Otherwise, B is the
762    possibly nonnull result of an earlier call to my_gmtime_r.  */
763 static long
764 gmtoff(struct tm const *a, ATTRIBUTE_MAYBE_UNUSED time_t *t,
765        ATTRIBUTE_MAYBE_UNUSED struct tm const *b)
766 {
767 #ifdef TM_GMTOFF
768   return a->TM_GMTOFF;
769 #else
770   struct tm tm;
771   if (t)
772     b = my_gmtime_r(t, &tm);
773   if (! b)
774     return LONG_MIN;
775   else {
776     int ayday = adjusted_yday(a, b);
777     int byday = adjusted_yday(b, a);
778     int days = ayday - byday;
779     long hours = a->tm_hour - b->tm_hour + 24 * days;
780     long minutes = a->tm_min - b->tm_min + 60 * hours;
781     long seconds = a->tm_sec - b->tm_sec + 60 * minutes;
782     return seconds;
783   }
784 #endif
785 }
786 
787 static void
788 show(timezone_t tz, char *zone, time_t t, bool v)
789 {
790 	register struct tm *	tmp;
791 	register struct tm *	gmtmp;
792 	struct tm tm, gmtm;
793 
794 	printf("%-*s  ", (int)longest, zone);
795 	if (v) {
796 		gmtmp = my_gmtime_r(&t, &gmtm);
797 		if (gmtmp == NULL) {
798 			printf(tformat(), t);
799 			printf(_(" (gmtime failed)"));
800 		} else {
801 			dumptime(gmtmp);
802 			printf(" UT");
803 		}
804 		printf(" = ");
805 	}
806 	tmp = localtime_rz(tz, &t, &tm);
807 	if (tmp == NULL) {
808 		printf(tformat(), t);
809 		printf(_(" (localtime failed)"));
810 	} else {
811 		dumptime(tmp);
812 		if (*abbr(tmp) != '\0')
813 			printf(" %s", abbr(tmp));
814 		if (v) {
815 			long off = gmtoff(tmp, NULL, gmtmp);
816 			printf(" isdst=%d", tmp->tm_isdst);
817 			if (off != LONG_MIN)
818 			  printf(" gmtoff=%ld", off);
819 		}
820 	}
821 	printf("\n");
822 	if (tmp != NULL && *abbr(tmp) != '\0')
823 		abbrok(abbr(tmp), zone);
824 }
825 
826 /* Show timestamps just before and just after a transition between
827    defined and undefined (or vice versa) in either localtime or
828    gmtime.  These transitions are for timezone TZ with name ZONE, in
829    the range from LO (with broken-down time LOTMP if that is nonnull)
830    through HI.  LO and HI disagree on definedness.  */
831 
832 static void
833 showextrema(timezone_t tz, char *zone, time_t lo, struct tm *lotmp, time_t hi)
834 {
835   struct tm localtm[2], gmtm[2];
836   time_t t, boundary = hunt(tz, lo, hi, true);
837   bool old = false;
838   hi = (SECSPERDAY < hi - boundary
839 	? boundary + SECSPERDAY
840 	: hi + (hi < TIME_T_MAX));
841   if (SECSPERDAY < boundary - lo) {
842     lo = boundary - SECSPERDAY;
843     lotmp = localtime_rz(tz, &lo, &localtm[old]);
844   }
845   if (lotmp)
846     localtm[old] = *lotmp;
847   else
848     localtm[old].tm_sec = -1;
849   if (! my_gmtime_r(&lo, &gmtm[old]))
850     gmtm[old].tm_sec = -1;
851 
852   /* Search sequentially for definedness transitions.  Although this
853      could be sped up by refining 'hunt' to search for either
854      localtime or gmtime definedness transitions, it hardly seems
855      worth the trouble.  */
856   for (t = lo + 1; t < hi; t++) {
857     bool new = !old;
858     if (! localtime_rz(tz, &t, &localtm[new]))
859       localtm[new].tm_sec = -1;
860     if (! my_gmtime_r(&t, &gmtm[new]))
861       gmtm[new].tm_sec = -1;
862     if (((localtm[old].tm_sec < 0) != (localtm[new].tm_sec < 0))
863 	| ((gmtm[old].tm_sec < 0) != (gmtm[new].tm_sec < 0))) {
864       show(tz, zone, t - 1, true);
865       show(tz, zone, t, true);
866     }
867     old = new;
868   }
869 }
870 
871 /* On pre-C99 platforms, a snprintf substitute good enough for us.  */
872 #if !HAVE_SNPRINTF
873 # include <stdarg.h>
874 ATTRIBUTE_FORMAT((printf, 3, 4)) static int
875 my_snprintf(char *s, size_t size, char const *format, ...)
876 {
877   int n;
878   va_list args;
879   char const *arg;
880   char *cp;
881   size_t arglen, slen;
882   char buf[1024];
883   va_start(args, format);
884   if (strcmp(format, "%s") == 0) {
885     arg = va_arg(args, char const *);
886     arglen = strlen(arg);
887   } else {
888     n = vsprintf(buf, format, args);
889     if (n < 0) {
890       va_end(args);
891       return n;
892     }
893     arg = buf;
894     arglen = n;
895   }
896   slen = arglen < size ? arglen : size - 1;
897   cp = s;
898   cp = mempcpy(cp, arg, slen);
899   *cp = '\0';
900   n = arglen <= INT_MAX ? arglen : -1;
901   va_end(args);
902   return n;
903 }
904 # define snprintf my_snprintf
905 #endif
906 
907 /* Store into BUF, of size SIZE, a formatted local time taken from *TM.
908    Use ISO 8601 format +HH:MM:SS.  Omit :SS if SS is zero, and omit
909    :MM too if MM is also zero.
910 
911    Return the length of the resulting string.  If the string does not
912    fit, return the length that the string would have been if it had
913    fit; do not overrun the output buffer.  */
914 static int
915 format_local_time(char *buf, ptrdiff_t size, struct tm const *tm)
916 {
917   int ss = tm->tm_sec, mm = tm->tm_min, hh = tm->tm_hour;
918   return (ss
919 	  ? snprintf(buf, size, "%02d:%02d:%02d", hh, mm, ss)
920 	  : mm
921 	  ? snprintf(buf, size, "%02d:%02d", hh, mm)
922 	  : snprintf(buf, size, "%02d", hh));
923 }
924 
925 /* Store into BUF, of size SIZE, a formatted UT offset for the
926    localtime *TM corresponding to time T.  Use ISO 8601 format
927    +HHMMSS, or -HHMMSS for timestamps west of Greenwich; use the
928    format -00 for unknown UT offsets.  If the hour needs more than
929    two digits to represent, extend the length of HH as needed.
930    Otherwise, omit SS if SS is zero, and omit MM too if MM is also
931    zero.
932 
933    Return the length of the resulting string, or -1 if the result is
934    not representable as a string.  If the string does not fit, return
935    the length that the string would have been if it had fit; do not
936    overrun the output buffer.  */
937 static int
938 format_utc_offset(char *buf, ptrdiff_t size, struct tm const *tm, time_t t)
939 {
940   long off = gmtoff(tm, &t, NULL);
941   char sign = ((off < 0
942 		|| (off == 0
943 		    && (*abbr(tm) == '-' || strcmp(abbr(tm), "zzz") == 0)))
944 	       ? '-' : '+');
945   long hh;
946   int mm, ss;
947   if (off < 0)
948     {
949       if (off == LONG_MIN)
950 	return -1;
951       off = -off;
952     }
953   ss = off % 60;
954   mm = off / 60 % 60;
955   hh = off / 60 / 60;
956   return (ss || 100 <= hh
957 	  ? snprintf(buf, size, "%c%02ld%02d%02d", sign, hh, mm, ss)
958 	  : mm
959 	  ? snprintf(buf, size, "%c%02ld%02d", sign, hh, mm)
960 	  : snprintf(buf, size, "%c%02ld", sign, hh));
961 }
962 
963 /* Store into BUF (of size SIZE) a quoted string representation of P.
964    If the representation's length is less than SIZE, return the
965    length; the representation is not null terminated.  Otherwise
966    return SIZE, to indicate that BUF is too small.  */
967 static ptrdiff_t
968 format_quoted_string(char *buf, ptrdiff_t size, char const *p)
969 {
970   char *b = buf;
971   ptrdiff_t s = size;
972   if (!s)
973     return size;
974   *b++ = '"', s--;
975   for (;;) {
976     char c = *p++;
977     if (s <= 1)
978       return size;
979     switch (c) {
980     default: *b++ = c, s--; continue;
981     case '\0': *b++ = '"', s--; return size - s;
982     case '"': case '\\': break;
983     case ' ': c = 's'; break;
984     case '\f': c = 'f'; break;
985     case '\n': c = 'n'; break;
986     case '\r': c = 'r'; break;
987     case '\t': c = 't'; break;
988     case '\v': c = 'v'; break;
989     }
990     *b++ = '\\', *b++ = c, s -= 2;
991   }
992 }
993 
994 /* Store into BUF (of size SIZE) a timestamp formatted by TIME_FMT.
995    TM is the broken-down time, T the seconds count, AB the time zone
996    abbreviation, and ZONE_NAME the zone name.  Return true if
997    successful, false if the output would require more than SIZE bytes.
998    TIME_FMT uses the same format that strftime uses, with these
999    additions:
1000 
1001    %f zone name
1002    %L local time as per format_local_time
1003    %Q like "U\t%Z\tD" where U is the UT offset as for format_utc_offset
1004       and D is the isdst flag; except omit D if it is zero, omit %Z if
1005       it equals U, quote and escape %Z if it contains nonalphabetics,
1006       and omit any trailing tabs.  */
1007 
1008 static bool
1009 istrftime(char *buf, ptrdiff_t size, char const *time_fmt,
1010 	  struct tm const *tm, time_t t, char const *ab, char const *zone_name)
1011 {
1012   char *b = buf;
1013   ptrdiff_t s = size;
1014   char const *f = time_fmt, *p;
1015 
1016   for (p = f; ; p++)
1017     if (*p == '%' && p[1] == '%')
1018       p++;
1019     else if (!*p
1020 	     || (*p == '%'
1021 		 && (p[1] == 'f' || p[1] == 'L' || p[1] == 'Q'))) {
1022       ptrdiff_t formatted_len;
1023       ptrdiff_t f_prefix_len = p - f;
1024       ptrdiff_t f_prefix_copy_size = sumsize(f_prefix_len, 2);
1025       char fbuf[100];
1026       bool oversized = sizeof fbuf <= (size_t)f_prefix_copy_size;
1027       char *f_prefix_copy = oversized ? xmalloc(f_prefix_copy_size) : fbuf;
1028       char *cp = f_prefix_copy;
1029       cp = mempcpy(cp, f, f_prefix_len);
1030       strcpy(cp, "X");
1031       formatted_len = strftime(b, s, f_prefix_copy, tm);
1032       if (oversized)
1033 	free(f_prefix_copy);
1034       if (formatted_len == 0)
1035 	return false;
1036       formatted_len--;
1037       b += formatted_len, s -= formatted_len;
1038       if (!*p++)
1039 	break;
1040       switch (*p) {
1041       case 'f':
1042 	formatted_len = format_quoted_string(b, s, zone_name);
1043 	break;
1044       case 'L':
1045 	formatted_len = format_local_time(b, s, tm);
1046 	break;
1047       case 'Q':
1048 	{
1049 	  bool show_abbr;
1050 	  int offlen = format_utc_offset(b, s, tm, t);
1051 	  if (! (0 <= offlen && offlen < s))
1052 	    return false;
1053 	  show_abbr = strcmp(b, ab) != 0;
1054 	  b += offlen, s -= offlen;
1055 	  if (show_abbr) {
1056 	    char const *abp;
1057 	    ptrdiff_t len;
1058 	    if (s <= 1)
1059 	      return false;
1060 	    *b++ = '\t', s--;
1061 	    for (abp = ab; is_alpha(*abp); abp++)
1062 	      continue;
1063 	    len = (!*abp && *ab
1064 		   ? snprintf(b, s, "%s", ab)
1065 		   : format_quoted_string(b, s, ab));
1066 	    if (s <= len)
1067 	      return false;
1068 	    b += len, s -= len;
1069 	  }
1070 	  formatted_len
1071 	    = (tm->tm_isdst
1072 	       ? snprintf(b, s, &"\t\t%d"[show_abbr], tm->tm_isdst)
1073 	       : 0);
1074 	}
1075 	break;
1076       }
1077       if (s <= formatted_len)
1078 	return false;
1079       b += formatted_len, s -= formatted_len;
1080       f = p + 1;
1081     }
1082   *b = '\0';
1083   return true;
1084 }
1085 
1086 /* Show a time transition.  */
1087 static void
1088 showtrans(char const *time_fmt, struct tm const *tm, time_t t, char const *ab,
1089 	  char const *zone_name)
1090 {
1091   if (!tm) {
1092     printf(tformat(), t);
1093     putchar('\n');
1094   } else {
1095     char stackbuf[1000];
1096     ptrdiff_t size = sizeof stackbuf;
1097     char *buf = stackbuf;
1098     char *bufalloc = NULL;
1099     while (! istrftime(buf, size, time_fmt, tm, t, ab, zone_name)) {
1100       size = sumsize(size, size);
1101       free(bufalloc);
1102       buf = bufalloc = xmalloc(size);
1103     }
1104     puts(buf);
1105     free(bufalloc);
1106   }
1107 }
1108 
1109 static char const *
1110 abbr(struct tm const *tmp)
1111 {
1112 #ifdef TM_ZONE
1113 	return tmp->TM_ZONE;
1114 #else
1115 # if HAVE_TZNAME
1116 	if (0 <= tmp->tm_isdst && tzname[0 < tmp->tm_isdst])
1117 	  return tzname[0 < tmp->tm_isdst];
1118 # endif
1119 	return "";
1120 #endif
1121 }
1122 
1123 /*
1124 ** The code below can fail on certain theoretical systems;
1125 ** it works on all known real-world systems as of 2022-01-25.
1126 */
1127 
1128 static const char *
1129 tformat(void)
1130 {
1131 #if HAVE__GENERIC
1132 	/* C11-style _Generic is more likely to return the correct
1133 	   format when distinct types have the same size.  */
1134 	char const *fmt =
1135 	  _Generic(+ (time_t) 0,
1136 		   int: "%d", long: "%ld", long long: "%lld",
1137 		   unsigned: "%u", unsigned long: "%lu",
1138 		   unsigned long long: "%llu",
1139 		   default: NULL);
1140 	if (fmt)
1141 	  return fmt;
1142 	fmt = _Generic((time_t) 0,
1143 		       intmax_t: "%"PRIdMAX, uintmax_t: "%"PRIuMAX,
1144 		       default: NULL);
1145 	if (fmt)
1146 	  return fmt;
1147 #endif
1148 	if (0 > (time_t) -1) {		/* signed */
1149 		if (sizeof(time_t) == sizeof(intmax_t))
1150 			return "%"PRIdMAX;
1151 		if (sizeof(time_t) > sizeof(long))
1152 			return "%lld";
1153 		if (sizeof(time_t) > sizeof(int))
1154 			return "%ld";
1155 		return "%d";
1156 	}
1157 #ifdef PRIuMAX
1158 	if (sizeof(time_t) == sizeof(uintmax_t))
1159 		return "%"PRIuMAX;
1160 #endif
1161 	if (sizeof(time_t) > sizeof(unsigned long))
1162 		return "%llu";
1163 	if (sizeof(time_t) > sizeof(unsigned int))
1164 		return "%lu";
1165 	return "%u";
1166 }
1167 
1168 static void
1169 dumptime(register const struct tm *timeptr)
1170 {
1171 	static const char	wday_name[][4] = {
1172 		"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
1173 	};
1174 	static const char	mon_name[][4] = {
1175 		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
1176 		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
1177 	};
1178 	register int		lead;
1179 	register int		trail;
1180 	int DIVISOR = 10;
1181 
1182 	/*
1183 	** The packaged localtime_rz and gmtime_r never put out-of-range
1184 	** values in tm_wday or tm_mon, but since this code might be compiled
1185 	** with other (perhaps experimental) versions, paranoia is in order.
1186 	*/
1187 	printf("%s %s%3d %.2d:%.2d:%.2d ",
1188 		((0 <= timeptr->tm_wday
1189 		  && timeptr->tm_wday < (int)(sizeof wday_name / sizeof wday_name[0]))
1190 		 ? wday_name[timeptr->tm_wday] : "???"),
1191 		((0 <= timeptr->tm_mon
1192 		  && timeptr->tm_mon < (int)(sizeof mon_name / sizeof mon_name[0]))
1193 		 ? mon_name[timeptr->tm_mon] : "???"),
1194 		timeptr->tm_mday, timeptr->tm_hour,
1195 		timeptr->tm_min, timeptr->tm_sec);
1196 	trail = timeptr->tm_year % DIVISOR + TM_YEAR_BASE % DIVISOR;
1197 	lead = timeptr->tm_year / DIVISOR + TM_YEAR_BASE / DIVISOR +
1198 		trail / DIVISOR;
1199 	trail %= DIVISOR;
1200 	if (trail < 0 && lead > 0) {
1201 		trail += DIVISOR;
1202 		--lead;
1203 	} else if (lead < 0 && trail > 0) {
1204 		trail -= DIVISOR;
1205 		++lead;
1206 	}
1207 	if (lead == 0)
1208 		printf("%d", trail);
1209 	else	printf("%d%d", lead, ((trail < 0) ? -trail : trail));
1210 }
1211