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