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