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