diff mbox series

[COMMITTED] timezone: sync to TZDB 2024a

Message ID 20240407203726.3135409-1-eggert@cs.ucla.edu
State New
Headers show
Series [COMMITTED] timezone: sync to TZDB 2024a | expand

Commit Message

Paul Eggert April 7, 2024, 8:37 p.m. UTC
Sync tzselect, zdump, zic to TZDB 2024a.
This patch incorporates the following TZDB source code changes,
listed roughly in descending order of importance.

  zic now supports links to links, needed for future tzdata
  zic now defaults to '-b slim'
  zic now updates output files atomically
  zic has new options -R, -l -, -p -
  zic -r now uses -00 for unspecified timestamps
  zdump now uses [lo,hi) for both -c and -t
  Fix several integer overflow bugs
  zic now checks input bytes more carefully
  Simplify and fix new TZDIR setup
  Default time_t to 64 bits on glibc 2.34+ 32-bit
  zic now generates TZ strings that conform to POSIX when all-year DST
  zic -v now shows extreme-int tm_year transitions
  Fix zic bug in last time type of Asia/Gaza etc.
  Fix zic bug with Palestine after 2075
  Fix bug uncovered by recent change to Iran history
  Fix 'zic -b fat' bug with Port Moresby 32-bit data
  Fix zic bug with -r @X where X is deduced from TZ
  Fix bug with zic -r cutoff before 1st transition
  Fix leap second expiry and truncation
  Fix zic bug on Linux 2.6.16 and 2.6.17
  Fix bug with 'zic -d /a/b/c' if /a is unwriteable
  Don't mistruncate TZif files at leap seconds
  Fix zdump undefined behavior if !USE_LTZ
  zdump -v reports localtime+gmtime failures better
  Fix zdump diagnostic for missing timezone
  Don't assume nonempty argv
  Port better to C23
  Do not assume negative >> behavior
  I18nize zdump a bit better
  Port zdump to right_only installations
  New tzselect menu option 'now'
  tzselect can now use current time to help choose
  Improve tzselect behavior for Turkey etc.
  tzselect: do not create temporary files
  tzselect: work around mawk bug with {2,}
  tzselect: Port to POSIX awk, which prohibits -v newlines
  Do not use empty RE in tzselect
  Don't set TZ in tzselect
  Avoid sed, expr in tzselect
  tzselect: Fix problems with spaces in TZDIR
  Improve tzselect diagnostics
  Remove zic workaround for Qt bug 53071
  Remove zic support for "min" in Rule lines
  Remove zic support for zic -y, Rule TYPEs, pacificnew
  Remove tzselect workaround for Bash 1.14.7 bug

* SHARED-FILES: Update to match current sync.
* config.h.in (HAVE_STRERROR): Remove; no longer needed.
* timezone/Makefile ($(objpfx)zic.o): Depend on tzdir.h.
($(objpfx)tzdir.h): New rule to build a placeholder.
* timezone/private.h, timezone/tzfile.h, timezone/version:
* timezone/zdump.c, timezone/zic.c: Copy verbatim from TZDB 2024a.
---
 SHARED-FILES          |   14 +-
 config.h.in           |    4 -
 timezone/Makefile     |    7 +
 timezone/private.h    |  597 +++++++----
 timezone/tzfile.h     |   32 +-
 timezone/tzselect.ksh |  923 ++++++++++-------
 timezone/version      |    2 +-
 timezone/zdump.c      |  453 ++++++---
 timezone/zic.c        | 2210 +++++++++++++++++++++++++----------------
 9 files changed, 2687 insertions(+), 1555 deletions(-)
diff mbox series

Patch

diff --git a/SHARED-FILES b/SHARED-FILES
index 07b7c7221f..9c8e715fb5 100644
--- a/SHARED-FILES
+++ b/SHARED-FILES
@@ -177,23 +177,25 @@  unicode:
 # The following files are shared with the upstream tzcode project and must be
 # updated regularly to stay in sync with the upstream releases.
 #
-# Update from tzcode 2017b.
-# Latest is 2018g:
-#   https://mm.icann.org/pipermail/tz-announce/2018-October/000052.html
+# Currently synced to TZDB 2024a, announced and distributed here:
+#	https://mm.icann.org/pipermail/tz-announce/2024-February/000081.html
+#	https://data.iana.org/time-zones/releases/tzdb-2024a.tar.lz
 tzcode:
   timezone/private.h
   timezone/tzfile.h
+  timezone/tzselect.ksh
+  timezone/version
   timezone/zdump.c
   timezone/zic.c
-  timezone/tzselect.ksh
 
 # The following files are shared with the upstream tzdata project but is not
 # synchronized regularly. The data files themselves are used only for testing
 # purposes and their data is never used to generate any output. We synchronize
 # them only to stay on top of newer data that might help with testing.
 #
-# Currently synced to 2009i.  Latest is 2018g.
-# https://mm.icann.org/pipermail/tz-announce/2018-October/000052.html
+# Currently synced to tzcode 2009i, announced and distributed here:
+#	https://mm.icann.org/pipermail/tz/2009-June/040697.html
+#	https://data.iana.org/time-zones/releases/tzdata2009i.tar.gz
 tzdata:
   timezone/africa
   timezone/antarctica
diff --git a/config.h.in b/config.h.in
index fd1499e9e2..69ac450356 100644
--- a/config.h.in
+++ b/config.h.in
@@ -240,10 +240,6 @@ 
 
 #ifdef	_LIBC
 
-/* The zic and zdump programs need these definitions.  */
-
-#define	HAVE_STRERROR	1
-
 /* The locale code needs these definitions.  */
 
 #define HAVE_REGEX 1
diff --git a/timezone/Makefile b/timezone/Makefile
index d7acb387ba..cf4ef3bf7e 100644
--- a/timezone/Makefile
+++ b/timezone/Makefile
@@ -49,6 +49,13 @@  endif
 include ../Rules
 
 
+$(objpfx)zic.o: $(objpfx)tzdir.h
+
+# In glibc this file is an empty placeholder,
+# as tz-cflags defines TZDEFAULT and TZDIR.
+$(objpfx)tzdir.h:
+	> $@
+
 $(objpfx)zic.o $(objpfx)zdump.o: $(objpfx)version.h
 
 $(objpfx)version.h: $(common-objpfx)config.make
diff --git a/timezone/private.h b/timezone/private.h
index 8513663036..0dac6af4e3 100644
--- a/timezone/private.h
+++ b/timezone/private.h
@@ -17,6 +17,40 @@ 
 ** Thank you!
 */
 
+/* PORT_TO_C89 means the code should work even if the underlying
+   compiler and library support only C89 plus C99's 'long long'
+   and perhaps a few other extensions to C89.  SUPPORT_C89 means the
+   tzcode library should support C89 callers in addition to the usual
+   support for C99-and-later callers; however, C89 support can trigger
+   latent bugs in C99-and-later callers.  These macros are obsolescent,
+   and the plan is to remove them along with any code needed only when
+   they are nonzero.  A good time to do that might be in the year 2029
+   because RHEL 7 (whose GCC defaults to C89) extended life cycle
+   support (ELS) is scheduled to end on 2028-06-30.  */
+#ifndef PORT_TO_C89
+# define PORT_TO_C89 0
+#endif
+#ifndef SUPPORT_C89
+# define SUPPORT_C89 0
+#endif
+
+#ifndef __STDC_VERSION__
+# define __STDC_VERSION__ 0
+#endif
+
+/* Define true, false and bool if they don't work out of the box.  */
+#if PORT_TO_C89 && __STDC_VERSION__ < 199901
+# define true 1
+# define false 0
+# define bool int
+#elif __STDC_VERSION__ < 202311
+# include <stdbool.h>
+#endif
+
+#if __STDC_VERSION__ < 202311
+# define static_assert(cond) extern int static_assert_check[(cond) ? 1 : -1]
+#endif
+
 /*
 ** zdump has been made independent of the rest of the time
 ** conversion package to increase confidence in the verification it provides.
@@ -36,79 +70,84 @@ 
 */
 
 #ifndef HAVE_DECL_ASCTIME_R
-#define HAVE_DECL_ASCTIME_R 1
+# define HAVE_DECL_ASCTIME_R 1
 #endif
 
-#if !defined HAVE_GENERIC && defined __has_extension
-# if __has_extension(c_generic_selections)
-#  define HAVE_GENERIC 1
-# else
-#  define HAVE_GENERIC 0
+#if !defined HAVE__GENERIC && defined __has_extension
+# if !__has_extension(c_generic_selections)
+#  define HAVE__GENERIC 0
 # endif
 #endif
 /* _Generic is buggy in pre-4.9 GCC.  */
-#if !defined HAVE_GENERIC && defined __GNUC__
-# define HAVE_GENERIC (4 < __GNUC__ + (9 <= __GNUC_MINOR__))
+#if !defined HAVE__GENERIC && defined __GNUC__ && !defined __STRICT_ANSI__
+# define HAVE__GENERIC (4 < __GNUC__ + (9 <= __GNUC_MINOR__))
 #endif
-#ifndef HAVE_GENERIC
-# define HAVE_GENERIC (201112 <= __STDC_VERSION__)
+#ifndef HAVE__GENERIC
+# define HAVE__GENERIC (201112 <= __STDC_VERSION__)
 #endif
 
+#if !defined HAVE_GETTEXT && defined __has_include
+# if __has_include(<libintl.h>)
+#  define HAVE_GETTEXT true
+# endif
+#endif
 #ifndef HAVE_GETTEXT
-#define HAVE_GETTEXT		0
-#endif /* !defined HAVE_GETTEXT */
+# define HAVE_GETTEXT false
+#endif
 
 #ifndef HAVE_INCOMPATIBLE_CTIME_R
-#define HAVE_INCOMPATIBLE_CTIME_R	0
+# define HAVE_INCOMPATIBLE_CTIME_R 0
 #endif
 
 #ifndef HAVE_LINK
-#define HAVE_LINK		1
+# define HAVE_LINK 1
 #endif /* !defined HAVE_LINK */
 
-#ifndef HAVE_POSIX_DECLS
-#define HAVE_POSIX_DECLS 1
+#ifndef HAVE_MALLOC_ERRNO
+# define HAVE_MALLOC_ERRNO 1
 #endif
 
-#ifndef HAVE_STDBOOL_H
-#define HAVE_STDBOOL_H (199901 <= __STDC_VERSION__)
+#ifndef HAVE_POSIX_DECLS
+# define HAVE_POSIX_DECLS 1
 #endif
 
-#ifndef HAVE_STRDUP
-#define HAVE_STRDUP 1
+#ifndef HAVE_SETENV
+# define HAVE_SETENV 1
 #endif
 
-#ifndef HAVE_STRTOLL
-#define HAVE_STRTOLL 1
+#ifndef HAVE_STRDUP
+# define HAVE_STRDUP 1
 #endif
 
 #ifndef HAVE_SYMLINK
-#define HAVE_SYMLINK		1
+# define HAVE_SYMLINK 1
 #endif /* !defined HAVE_SYMLINK */
 
+#if !defined HAVE_SYS_STAT_H && defined __has_include
+# if !__has_include(<sys/stat.h>)
+#  define HAVE_SYS_STAT_H false
+# endif
+#endif
 #ifndef HAVE_SYS_STAT_H
-#define HAVE_SYS_STAT_H		1
-#endif /* !defined HAVE_SYS_STAT_H */
-
-#ifndef HAVE_SYS_WAIT_H
-#define HAVE_SYS_WAIT_H		1
-#endif /* !defined HAVE_SYS_WAIT_H */
+# define HAVE_SYS_STAT_H true
+#endif
 
+#if !defined HAVE_UNISTD_H && defined __has_include
+# if !__has_include(<unistd.h>)
+#  define HAVE_UNISTD_H false
+# endif
+#endif
 #ifndef HAVE_UNISTD_H
-#define HAVE_UNISTD_H		1
-#endif /* !defined HAVE_UNISTD_H */
-
-#ifndef HAVE_UTMPX_H
-#define HAVE_UTMPX_H		1
-#endif /* !defined HAVE_UTMPX_H */
+# define HAVE_UNISTD_H true
+#endif
 
 #ifndef NETBSD_INSPIRED
 # define NETBSD_INSPIRED 1
 #endif
 
 #if HAVE_INCOMPATIBLE_CTIME_R
-#define asctime_r _incompatible_asctime_r
-#define ctime_r _incompatible_ctime_r
+# define asctime_r _incompatible_asctime_r
+# define ctime_r _incompatible_ctime_r
 #endif /* HAVE_INCOMPATIBLE_CTIME_R */
 
 /* Enable tm_gmtoff, tm_zone, and environ on GNUish systems.  */
@@ -118,15 +157,17 @@ 
 /* Enable strtoimax on pre-C99 Solaris 11.  */
 #define __EXTENSIONS__ 1
 
-/* To avoid having 'stat' fail unnecessarily with errno == EOVERFLOW,
-   enable large files on GNUish systems ...  */
+/* On GNUish systems where time_t might be 32 or 64 bits, use 64.
+   On these platforms _FILE_OFFSET_BITS must also be 64; otherwise
+   setting _TIME_BITS to 64 does not work.  The code does not
+   otherwise rely on _FILE_OFFSET_BITS being 64, since it does not
+   use off_t or functions like 'stat' that depend on off_t.  */
 #ifndef _FILE_OFFSET_BITS
 # define _FILE_OFFSET_BITS 64
 #endif
-/* ... and on AIX ...  */
-#define _LARGE_FILES 1
-/* ... and enable large inode numbers on Mac OS X 10.5 and later.  */
-#define _DARWIN_USE_64_BIT_INODE 1
+#if !defined _TIME_BITS && _FILE_OFFSET_BITS == 64
+# define _TIME_BITS 64
+#endif
 
 /*
 ** Nested includes
@@ -157,16 +198,29 @@ 
 #undef tzalloc
 #undef tzfree
 
-#include <sys/types.h>	/* for time_t */
+#include <stddef.h>
 #include <string.h>
+#if !PORT_TO_C89
+# include <inttypes.h>
+#endif
 #include <limits.h>	/* for CHAR_BIT et al. */
 #include <stdlib.h>
 
 #include <errno.h>
 
+#ifndef EINVAL
+# define EINVAL ERANGE
+#endif
+
+#ifndef ELOOP
+# define ELOOP EINVAL
+#endif
 #ifndef ENAMETOOLONG
 # define ENAMETOOLONG EINVAL
 #endif
+#ifndef ENOMEM
+# define ENOMEM EINVAL
+#endif
 #ifndef ENOTSUP
 # define ENOTSUP EINVAL
 #endif
@@ -175,11 +229,11 @@ 
 #endif
 
 #if HAVE_GETTEXT
-#include <libintl.h>
+# include <libintl.h>
 #endif /* HAVE_GETTEXT */
 
 #if HAVE_UNISTD_H
-#include <unistd.h>	/* for R_OK, and other POSIX goodness */
+# include <unistd.h> /* for R_OK, and other POSIX goodness */
 #endif /* HAVE_UNISTD_H */
 
 #ifndef HAVE_STRFTIME_L
@@ -215,27 +269,29 @@ 
 #endif
 
 #ifndef R_OK
-#define R_OK	4
+# define R_OK 4
 #endif /* !defined R_OK */
 
-/* Unlike <ctype.h>'s isdigit, this also works if c < 0 | c > UCHAR_MAX. */
-#define is_digit(c) ((unsigned)(c) - '0' <= 9)
+#if PORT_TO_C89
 
 /*
 ** Define HAVE_STDINT_H's default value here, rather than at the
 ** start, since __GLIBC__ and INTMAX_MAX's values depend on
-** previously-included files.  glibc 2.1 and Solaris 10 and later have
+** previously included files.  glibc 2.1 and Solaris 10 and later have
 ** stdint.h, even with pre-C99 compilers.
 */
+#if !defined HAVE_STDINT_H && defined __has_include
+# define HAVE_STDINT_H true /* C23 __has_include implies C99 stdint.h.  */
+#endif
 #ifndef HAVE_STDINT_H
-#define HAVE_STDINT_H \
+# define HAVE_STDINT_H \
    (199901 <= __STDC_VERSION__ \
-    || 2 < __GLIBC__ + (1 <= __GLIBC_MINOR__)	\
+    || 2 < __GLIBC__ + (1 <= __GLIBC_MINOR__) \
     || __CYGWIN__ || INTMAX_MAX)
 #endif /* !defined HAVE_STDINT_H */
 
 #if HAVE_STDINT_H
-#include <stdint.h>
+# include <stdint.h>
 #endif /* !HAVE_STDINT_H */
 
 #ifndef HAVE_INTTYPES_H
@@ -246,36 +302,36 @@ 
 #endif
 
 /* Pre-C99 GCC compilers define __LONG_LONG_MAX__ instead of LLONG_MAX.  */
-#ifdef __LONG_LONG_MAX__
+#if defined __LONG_LONG_MAX__ && !defined __STRICT_ANSI__
 # ifndef LLONG_MAX
 #  define LLONG_MAX __LONG_LONG_MAX__
 # endif
 # ifndef LLONG_MIN
 #  define LLONG_MIN (-1 - LLONG_MAX)
 # endif
+# ifndef ULLONG_MAX
+#  define ULLONG_MAX (LLONG_MAX * 2ull + 1)
+# endif
 #endif
 
 #ifndef INT_FAST64_MAX
-# ifdef LLONG_MAX
-typedef long long	int_fast64_t;
-#  define INT_FAST64_MIN LLONG_MIN
-#  define INT_FAST64_MAX LLONG_MAX
-# else
-#  if LONG_MAX >> 31 < 0xffffffff
-Please use a compiler that supports a 64-bit integer type (or wider);
-you may need to compile with "-DHAVE_STDINT_H".
-#  endif
-typedef long		int_fast64_t;
+# if 1 <= LONG_MAX >> 31 >> 31
+typedef long int_fast64_t;
 #  define INT_FAST64_MIN LONG_MIN
 #  define INT_FAST64_MAX LONG_MAX
+# else
+/* If this fails, compile with -DHAVE_STDINT_H or with a better compiler.  */
+typedef long long int_fast64_t;
+#  define INT_FAST64_MIN LLONG_MIN
+#  define INT_FAST64_MAX LLONG_MAX
 # endif
 #endif
 
 #ifndef PRIdFAST64
-# if INT_FAST64_MAX == LLONG_MAX
-#  define PRIdFAST64 "lld"
-# else
+# if INT_FAST64_MAX == LONG_MAX
 #  define PRIdFAST64 "ld"
+# else
+#  define PRIdFAST64 "lld"
 # endif
 #endif
 
@@ -298,6 +354,9 @@  typedef int int_fast32_t;
 #ifndef INTMAX_MAX
 # ifdef LLONG_MAX
 typedef long long intmax_t;
+#  ifndef HAVE_STRTOLL
+#   define HAVE_STRTOLL true
+#  endif
 #  if HAVE_STRTOLL
 #   define strtoimax strtoll
 #  endif
@@ -321,66 +380,183 @@  typedef long intmax_t;
 # endif
 #endif
 
+#ifndef PTRDIFF_MAX
+# define PTRDIFF_MAX MAXVAL(ptrdiff_t, TYPE_BIT(ptrdiff_t))
+#endif
+
+#ifndef UINT_FAST32_MAX
+typedef unsigned long uint_fast32_t;
+#endif
+
 #ifndef UINT_FAST64_MAX
-# if defined ULLONG_MAX || defined __LONG_LONG_MAX__
-typedef unsigned long long uint_fast64_t;
+# if 3 <= ULONG_MAX >> 31 >> 31
+typedef unsigned long uint_fast64_t;
+#  define UINT_FAST64_MAX ULONG_MAX
 # else
-#  if ULONG_MAX >> 31 >> 1 < 0xffffffff
-Please use a compiler that supports a 64-bit integer type (or wider);
-you may need to compile with "-DHAVE_STDINT_H".
-#  endif
-typedef unsigned long	uint_fast64_t;
+/* If this fails, compile with -DHAVE_STDINT_H or with a better compiler.  */
+typedef unsigned long long uint_fast64_t;
+#  define UINT_FAST64_MAX ULLONG_MAX
 # endif
 #endif
 
 #ifndef UINTMAX_MAX
-# if defined ULLONG_MAX || defined __LONG_LONG_MAX__
+# ifdef ULLONG_MAX
 typedef unsigned long long uintmax_t;
+#  define UINTMAX_MAX ULLONG_MAX
 # else
 typedef unsigned long uintmax_t;
+#  define UINTMAX_MAX ULONG_MAX
 # endif
 #endif
 
 #ifndef PRIuMAX
-# if defined ULLONG_MAX || defined __LONG_LONG_MAX__
+# ifdef ULLONG_MAX
 #  define PRIuMAX "llu"
 # else
 #  define PRIuMAX "lu"
 # endif
 #endif
 
-#ifndef INT32_MAX
-#define INT32_MAX 0x7fffffff
-#endif /* !defined INT32_MAX */
-#ifndef INT32_MIN
-#define INT32_MIN (-1 - INT32_MAX)
-#endif /* !defined INT32_MIN */
-
 #ifndef SIZE_MAX
-#define SIZE_MAX ((size_t) -1)
+# define SIZE_MAX ((size_t) -1)
+#endif
+
+#endif /* PORT_TO_C89 */
+
+/* The maximum size of any created object, as a signed integer.
+   Although the C standard does not outright prohibit larger objects,
+   behavior is undefined if the result of pointer subtraction does not
+   fit into ptrdiff_t, and the code assumes in several places that
+   pointer subtraction works.  As a practical matter it's OK to not
+   support objects larger than this.  */
+#define INDEX_MAX ((ptrdiff_t) min(PTRDIFF_MAX, SIZE_MAX))
+
+/* Support ckd_add, ckd_sub, ckd_mul on C23 or recent-enough GCC-like
+   hosts, unless compiled with -DHAVE_STDCKDINT_H=0 or with pre-C23 EDG.  */
+#if !defined HAVE_STDCKDINT_H && defined __has_include
+# if __has_include(<stdckdint.h>)
+#  define HAVE_STDCKDINT_H true
+# endif
+#endif
+#ifdef HAVE_STDCKDINT_H
+# if HAVE_STDCKDINT_H
+#  include <stdckdint.h>
+# endif
+#elif defined __EDG__
+/* Do nothing, to work around EDG bug <https://bugs.gnu.org/53256>.  */
+#elif defined __has_builtin
+# if __has_builtin(__builtin_add_overflow)
+#  define ckd_add(r, a, b) __builtin_add_overflow(a, b, r)
+# endif
+# if __has_builtin(__builtin_sub_overflow)
+#  define ckd_sub(r, a, b) __builtin_sub_overflow(a, b, r)
+# endif
+# if __has_builtin(__builtin_mul_overflow)
+#  define ckd_mul(r, a, b) __builtin_mul_overflow(a, b, r)
+# endif
+#elif 7 <= __GNUC__
+# define ckd_add(r, a, b) __builtin_add_overflow(a, b, r)
+# define ckd_sub(r, a, b) __builtin_sub_overflow(a, b, r)
+# define ckd_mul(r, a, b) __builtin_mul_overflow(a, b, r)
 #endif
 
 #if 3 <= __GNUC__
-# define ATTRIBUTE_CONST __attribute__ ((const))
-# define ATTRIBUTE_MALLOC __attribute__ ((__malloc__))
-# define ATTRIBUTE_PURE __attribute__ ((__pure__))
-# define ATTRIBUTE_FORMAT(spec) __attribute__ ((__format__ spec))
+# define ATTRIBUTE_MALLOC __attribute__((malloc))
+# define ATTRIBUTE_FORMAT(spec) __attribute__((format spec))
 #else
-# define ATTRIBUTE_CONST /* empty */
 # define ATTRIBUTE_MALLOC /* empty */
-# define ATTRIBUTE_PURE /* empty */
 # define ATTRIBUTE_FORMAT(spec) /* empty */
 #endif
 
-#if !defined _Noreturn && __STDC_VERSION__ < 201112
-# if 2 < __GNUC__ + (8 <= __GNUC_MINOR__)
-#  define _Noreturn __attribute__ ((__noreturn__))
+#if (defined __has_c_attribute \
+     && (202311 <= __STDC_VERSION__ || !defined __STRICT_ANSI__))
+# define HAVE___HAS_C_ATTRIBUTE true
+#else
+# define HAVE___HAS_C_ATTRIBUTE false
+#endif
+
+#if HAVE___HAS_C_ATTRIBUTE
+# if __has_c_attribute(deprecated)
+#  define ATTRIBUTE_DEPRECATED [[deprecated]]
+# endif
+#endif
+#ifndef ATTRIBUTE_DEPRECATED
+# if 3 < __GNUC__ + (2 <= __GNUC_MINOR__)
+#  define ATTRIBUTE_DEPRECATED __attribute__((deprecated))
+# else
+#  define ATTRIBUTE_DEPRECATED /* empty */
+# endif
+#endif
+
+#if HAVE___HAS_C_ATTRIBUTE
+# if __has_c_attribute(fallthrough)
+#  define ATTRIBUTE_FALLTHROUGH [[fallthrough]]
+# endif
+#endif
+#ifndef ATTRIBUTE_FALLTHROUGH
+# if 7 <= __GNUC__
+#  define ATTRIBUTE_FALLTHROUGH __attribute__((fallthrough))
 # else
-#  define _Noreturn
+#  define ATTRIBUTE_FALLTHROUGH ((void) 0)
 # endif
 #endif
 
-#if __STDC_VERSION__ < 199901 && !defined restrict
+#if HAVE___HAS_C_ATTRIBUTE
+# if __has_c_attribute(maybe_unused)
+#  define ATTRIBUTE_MAYBE_UNUSED [[maybe_unused]]
+# endif
+#endif
+#ifndef ATTRIBUTE_MAYBE_UNUSED
+# if 2 < __GNUC__ + (7 <= __GNUC_MINOR__)
+#  define ATTRIBUTE_MAYBE_UNUSED __attribute__((unused))
+# else
+#  define ATTRIBUTE_MAYBE_UNUSED /* empty */
+# endif
+#endif
+
+#if HAVE___HAS_C_ATTRIBUTE
+# if __has_c_attribute(noreturn)
+#  define ATTRIBUTE_NORETURN [[noreturn]]
+# endif
+#endif
+#ifndef ATTRIBUTE_NORETURN
+# if 201112 <= __STDC_VERSION__
+#  define ATTRIBUTE_NORETURN _Noreturn
+# elif 2 < __GNUC__ + (8 <= __GNUC_MINOR__)
+#  define ATTRIBUTE_NORETURN __attribute__((noreturn))
+# else
+#  define ATTRIBUTE_NORETURN /* empty */
+# endif
+#endif
+
+#if HAVE___HAS_C_ATTRIBUTE
+# if __has_c_attribute(reproducible)
+#  define ATTRIBUTE_REPRODUCIBLE [[reproducible]]
+# endif
+#endif
+#ifndef ATTRIBUTE_REPRODUCIBLE
+# if 3 <= __GNUC__
+#  define ATTRIBUTE_REPRODUCIBLE __attribute__((pure))
+# else
+#  define ATTRIBUTE_REPRODUCIBLE /* empty */
+# endif
+#endif
+
+#if HAVE___HAS_C_ATTRIBUTE
+# if __has_c_attribute(unsequenced)
+#  define ATTRIBUTE_UNSEQUENCED [[unsequenced]]
+# endif
+#endif
+#ifndef ATTRIBUTE_UNSEQUENCED
+# if 3 <= __GNUC__
+#  define ATTRIBUTE_UNSEQUENCED __attribute__((const))
+# else
+#  define ATTRIBUTE_UNSEQUENCED /* empty */
+# endif
+#endif
+
+#if (__STDC_VERSION__ < 199901 && !defined restrict \
+     && (PORT_TO_C89 || defined _MSC_VER))
 # define restrict /* empty */
 #endif
 
@@ -418,10 +594,11 @@  typedef unsigned long uintmax_t;
 # define TZ_TIME_T 0
 #endif
 
-#if TZ_TIME_T
-# ifdef LOCALTIME_IMPLEMENTATION
+#if defined LOCALTIME_IMPLEMENTATION && TZ_TIME_T
 static time_t sys_time(time_t *x) { return time(x); }
-# endif
+#endif
+
+#if TZ_TIME_T
 
 typedef time_tz tz_time_t;
 
@@ -477,8 +654,6 @@  typedef time_tz tz_time_t;
 # define tzfree tz_tzfree
 # undef  tzset
 # define tzset tz_tzset
-# undef  tzsetwall
-# define tzsetwall tz_tzsetwall
 # if HAVE_STRFTIME_L
 #  undef  strftime_l
 #  define strftime_l tz_strftime_l
@@ -498,11 +673,16 @@  typedef time_tz tz_time_t;
 #  define altzone tz_altzone
 # endif
 
-char *asctime(struct tm const *);
+# if __STDC_VERSION__ < 202311
+#  define DEPRECATED_IN_C23 /* empty */
+# else
+#  define DEPRECATED_IN_C23 ATTRIBUTE_DEPRECATED
+# endif
+DEPRECATED_IN_C23 char *asctime(struct tm const *);
 char *asctime_r(struct tm const *restrict, char *restrict);
-char *ctime(time_t const *);
+DEPRECATED_IN_C23 char *ctime(time_t const *);
 char *ctime_r(time_t const *, char *);
-double difftime(time_t, time_t) ATTRIBUTE_CONST;
+ATTRIBUTE_UNSEQUENCED double difftime(time_t, time_t);
 size_t strftime(char *restrict, size_t, char const *restrict,
 		struct tm const *restrict);
 # if HAVE_STRFTIME_L
@@ -515,9 +695,24 @@  struct tm *localtime(time_t const *);
 struct tm *localtime_r(time_t const *restrict, struct tm *restrict);
 time_t mktime(struct tm *);
 time_t time(time_t *);
+time_t timegm(struct tm *);
 void tzset(void);
 #endif
 
+#ifndef HAVE_DECL_TIMEGM
+# if (202311 <= __STDC_VERSION__ \
+      || defined __GLIBC__ || defined __tm_zone /* musl */ \
+      || defined __FreeBSD__ || defined __NetBSD__ || defined __OpenBSD__ \
+      || (defined __APPLE__ && defined __MACH__))
+#  define HAVE_DECL_TIMEGM true
+# else
+#  define HAVE_DECL_TIMEGM false
+# endif
+#endif
+#if !HAVE_DECL_TIMEGM && !defined timegm
+time_t timegm(struct tm *);
+#endif
+
 #if !HAVE_DECL_ASCTIME_R && !defined asctime_r
 extern char *asctime_r(struct tm const *restrict, char *restrict);
 #endif
@@ -550,21 +745,18 @@  extern long altzone;
 ** declarations if time_tz is defined.
 */
 
-#ifdef STD_INSPIRED
-# if TZ_TIME_T || !defined tzsetwall
-void tzsetwall(void);
-# endif
+#ifndef STD_INSPIRED
+# define STD_INSPIRED 0
+#endif
+#if STD_INSPIRED
 # if TZ_TIME_T || !defined offtime
 struct tm *offtime(time_t const *, long);
 # endif
-# if TZ_TIME_T || !defined timegm
-time_t timegm(struct tm *);
-# endif
 # if TZ_TIME_T || !defined timelocal
 time_t timelocal(struct tm *);
 # endif
 # if TZ_TIME_T || !defined timeoff
-time_t timeoff(struct tm *, long);
+#  define EXTERN_TIMEOFF
 # endif
 # if TZ_TIME_T || !defined time2posix
 time_t time2posix(time_t);
@@ -576,7 +768,9 @@  time_t posix2time(time_t);
 
 /* Infer TM_ZONE on systems where this information is known, but suppress
    guessing if NO_TM_ZONE is defined.  Similarly for TM_GMTOFF.  */
-#if (defined __GLIBC__ \
+#if (200809 < _POSIX_VERSION \
+     || defined __GLIBC__ \
+     || defined __tm_zone /* musl */ \
      || defined __FreeBSD__ || defined __NetBSD__ || defined __OpenBSD__ \
      || (defined __APPLE__ && defined __MACH__))
 # if !defined TM_GMTOFF && !defined NO_TM_GMTOFF
@@ -602,12 +796,12 @@  struct tm *localtime_rz(timezone_t restrict, time_t const *restrict,
 time_t mktime_z(timezone_t restrict, struct tm *restrict);
 timezone_t tzalloc(char const *);
 void tzfree(timezone_t);
-# ifdef STD_INSPIRED
+# if STD_INSPIRED
 #  if TZ_TIME_T || !defined posix2time_z
-time_t posix2time_z(timezone_t, time_t) ATTRIBUTE_PURE;
+ATTRIBUTE_REPRODUCIBLE time_t posix2time_z(timezone_t, time_t);
 #  endif
 #  if TZ_TIME_T || !defined time2posix_z
-time_t time2posix_z(timezone_t, time_t) ATTRIBUTE_PURE;
+ATTRIBUTE_REPRODUCIBLE time_t time2posix_z(timezone_t, time_t);
 #  endif
 # endif
 #endif
@@ -616,18 +810,15 @@  time_t time2posix_z(timezone_t, time_t) ATTRIBUTE_PURE;
 ** Finally, some convenience items.
 */
 
-#if HAVE_STDBOOL_H
-# include <stdbool.h>
-#else
-# define true 1
-# define false 0
-# define bool int
-#endif
-
-#define TYPE_BIT(type)	(sizeof (type) * CHAR_BIT)
+#define TYPE_BIT(type) (CHAR_BIT * (ptrdiff_t) sizeof(type))
 #define TYPE_SIGNED(type) (((type) -1) < 0)
 #define TWOS_COMPLEMENT(t) ((t) ~ (t) 0 < 0)
 
+/* Minimum and maximum of two values.  Use lower case to avoid
+   naming clashes with standard include files.  */
+#define max(a, b) ((a) > (b) ? (a) : (b))
+#define min(a, b) ((a) < (b) ? (a) : (b))
+
 /* Max and min values of the integer type T, of which only the bottom
    B bits are used, and where the highest-order used bit is considered
    to be a sign bit if T is signed.  */
@@ -642,12 +833,12 @@  time_t time2posix_z(timezone_t, time_t) ATTRIBUTE_PURE;
 #define TIME_T_MAX_NO_PADDING MAXVAL(time_t, TYPE_BIT(time_t))
 
 /* The extreme time values.  These are macros, not constants, so that
-   any portability problem occur only when compiling .c files that use
+   any portability problems occur only when compiling .c files that use
    the macros, which is safer for applications that need only zdump and zic.
    This implementation assumes no padding if time_t is signed and
    either the compiler lacks support for _Generic or time_t is not one
    of the standard signed integer types.  */
-#if HAVE_GENERIC
+#if HAVE__GENERIC
 # define TIME_T_MIN \
     _Generic((time_t) 0, \
 	     signed char: SCHAR_MIN, short: SHRT_MIN, \
@@ -660,10 +851,23 @@  time_t time2posix_z(timezone_t, time_t) ATTRIBUTE_PURE;
 		int: INT_MAX, long: LONG_MAX, long long: LLONG_MAX, \
 		default: TIME_T_MAX_NO_PADDING)			    \
      : (time_t) -1)
+enum { SIGNED_PADDING_CHECK_NEEDED
+         = _Generic((time_t) 0,
+		    signed char: false, short: false,
+		    int: false, long: false, long long: false,
+		    default: true) };
 #else
 # define TIME_T_MIN TIME_T_MIN_NO_PADDING
 # define TIME_T_MAX TIME_T_MAX_NO_PADDING
+enum { SIGNED_PADDING_CHECK_NEEDED = true };
 #endif
+/* Try to check the padding assumptions.  Although TIME_T_MAX and the
+   following check can both have undefined behavior on oddball
+   platforms due to shifts exceeding widths of signed integers, these
+   platforms' compilers are likely to diagnose these issues in integer
+   constant expressions, so it shouldn't hurt to check statically.  */
+static_assert(! TYPE_SIGNED(time_t) || ! SIGNED_PADDING_CHECK_NEEDED
+	      || TIME_T_MAX >> (TYPE_BIT(time_t) - 2) == 1);
 
 /*
 ** 302 / 1000 is log10(2.0) rounded up.
@@ -685,10 +889,45 @@  time_t time2posix_z(timezone_t, time_t) ATTRIBUTE_PURE;
 # define INITIALIZE(x)
 #endif
 
+/* Whether memory access must strictly follow the C standard.
+   If 0, it's OK to read uninitialized storage so long as the value is
+   not relied upon.  Defining it to 0 lets mktime access parts of
+   struct tm that might be uninitialized, as a heuristic when the
+   standard doesn't say what to return and when tm_gmtoff can help
+   mktime likely infer a better value.  */
 #ifndef UNINIT_TRAP
 # define UNINIT_TRAP 0
 #endif
 
+/* localtime.c sometimes needs access to timeoff if it is not already public.
+   tz_private_timeoff should be used only by localtime.c.  */
+#if (!defined EXTERN_TIMEOFF \
+     && defined TM_GMTOFF && (200809 < _POSIX_VERSION || ! UNINIT_TRAP))
+# ifndef timeoff
+#  define timeoff tz_private_timeoff
+# endif
+# define EXTERN_TIMEOFF
+#endif
+#ifdef EXTERN_TIMEOFF
+time_t timeoff(struct tm *, long);
+#endif
+
+#ifdef DEBUG
+# undef unreachable
+# define unreachable() abort()
+#elif !defined unreachable
+# ifdef __has_builtin
+#  if __has_builtin(__builtin_unreachable)
+#   define unreachable() __builtin_unreachable()
+#  endif
+# elif 4 < __GNUC__ + (5 <= __GNUC_MINOR__)
+#  define unreachable() __builtin_unreachable()
+# endif
+# ifndef unreachable
+#  define unreachable() ((void) 0)
+# endif
+#endif
+
 /*
 ** For the benefit of GNU folk...
 ** '_(MSGID)' uses the current locale's message library string for MSGID.
@@ -708,49 +947,73 @@  time_t time2posix_z(timezone_t, time_t) ATTRIBUTE_PURE;
 #if HAVE_INCOMPATIBLE_CTIME_R
 #undef asctime_r
 #undef ctime_r
-char *asctime_r(struct tm const *, char *);
+char *asctime_r(struct tm const *restrict, char *restrict);
 char *ctime_r(time_t const *, char *);
 #endif /* HAVE_INCOMPATIBLE_CTIME_R */
 
 /* Handy macros that are independent of tzfile implementation.  */
 
-#define YEARSPERREPEAT		400	/* years before a Gregorian repeat */
+enum {
+  SECSPERMIN = 60,
+  MINSPERHOUR = 60,
+  SECSPERHOUR = SECSPERMIN * MINSPERHOUR,
+  HOURSPERDAY = 24,
+  DAYSPERWEEK = 7,
+  DAYSPERNYEAR = 365,
+  DAYSPERLYEAR = DAYSPERNYEAR + 1,
+  MONSPERYEAR = 12,
+  YEARSPERREPEAT = 400	/* years before a Gregorian repeat */
+};
 
-#define SECSPERMIN	60
-#define MINSPERHOUR	60
-#define HOURSPERDAY	24
-#define DAYSPERWEEK	7
-#define DAYSPERNYEAR	365
-#define DAYSPERLYEAR	366
-#define SECSPERHOUR	(SECSPERMIN * MINSPERHOUR)
 #define SECSPERDAY	((int_fast32_t) SECSPERHOUR * HOURSPERDAY)
-#define MONSPERYEAR	12
-
-#define TM_SUNDAY	0
-#define TM_MONDAY	1
-#define TM_TUESDAY	2
-#define TM_WEDNESDAY	3
-#define TM_THURSDAY	4
-#define TM_FRIDAY	5
-#define TM_SATURDAY	6
-
-#define TM_JANUARY	0
-#define TM_FEBRUARY	1
-#define TM_MARCH	2
-#define TM_APRIL	3
-#define TM_MAY		4
-#define TM_JUNE		5
-#define TM_JULY		6
-#define TM_AUGUST	7
-#define TM_SEPTEMBER	8
-#define TM_OCTOBER	9
-#define TM_NOVEMBER	10
-#define TM_DECEMBER	11
-
-#define TM_YEAR_BASE	1900
-
-#define EPOCH_YEAR	1970
-#define EPOCH_WDAY	TM_THURSDAY
+
+#define DAYSPERREPEAT		((int_fast32_t) 400 * 365 + 100 - 4 + 1)
+#define SECSPERREPEAT		((int_fast64_t) DAYSPERREPEAT * SECSPERDAY)
+#define AVGSECSPERYEAR		(SECSPERREPEAT / YEARSPERREPEAT)
+
+/* How many years to generate (in zic.c) or search through (in localtime.c).
+   This is two years larger than the obvious 400, to avoid edge cases.
+   E.g., suppose a non-POSIX.1-2017 rule applies from 2012 on with transitions
+   in March and September, plus one-off transitions in November 2013.
+   If zic looked only at the last 400 years, it would set max_year=2413,
+   with the intent that the 400 years 2014 through 2413 will be repeated.
+   The last transition listed in the tzfile would be in 2413-09,
+   less than 400 years after the last one-off transition in 2013-11.
+   Two years is not overkill for localtime.c, as a one-year bump
+   would mishandle 2023d's America/Ciudad_Juarez for November 2422.  */
+enum { years_of_observations = YEARSPERREPEAT + 2 };
+
+enum {
+  TM_SUNDAY,
+  TM_MONDAY,
+  TM_TUESDAY,
+  TM_WEDNESDAY,
+  TM_THURSDAY,
+  TM_FRIDAY,
+  TM_SATURDAY
+};
+
+enum {
+  TM_JANUARY,
+  TM_FEBRUARY,
+  TM_MARCH,
+  TM_APRIL,
+  TM_MAY,
+  TM_JUNE,
+  TM_JULY,
+  TM_AUGUST,
+  TM_SEPTEMBER,
+  TM_OCTOBER,
+  TM_NOVEMBER,
+  TM_DECEMBER
+};
+
+enum {
+  TM_YEAR_BASE = 1900,
+  TM_WDAY_BASE = TM_MONDAY,
+  EPOCH_YEAR = 1970,
+  EPOCH_WDAY = TM_THURSDAY
+};
 
 #define isleap(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0))
 
@@ -768,14 +1031,4 @@  char *ctime_r(time_t const *, char *);
 
 #define isleap_sum(a, b)	isleap((a) % 400 + (b) % 400)
 
-
-/*
-** The Gregorian year averages 365.2425 days, which is 31556952 seconds.
-*/
-
-#define AVGSECSPERYEAR		31556952L
-#define SECSPERREPEAT \
-  ((int_fast64_t) YEARSPERREPEAT * (int_fast64_t) AVGSECSPERYEAR)
-#define SECSPERREPEAT_BITS	34	/* ceil(log2(SECSPERREPEAT)) */
-
 #endif /* !defined PRIVATE_H */
diff --git a/timezone/tzfile.h b/timezone/tzfile.h
index ee91104443..3155010ed1 100644
--- a/timezone/tzfile.h
+++ b/timezone/tzfile.h
@@ -21,16 +21,8 @@ 
 ** Information about time zone files.
 */
 
-#ifndef TZDIR
-#define TZDIR	"/usr/share/zoneinfo" /* Time zone object file directory */
-#endif /* !defined TZDIR */
-
-#ifndef TZDEFAULT
-#define TZDEFAULT	"/etc/localtime"
-#endif /* !defined TZDEFAULT */
-
 #ifndef TZDEFRULES
-#define TZDEFRULES	"posixrules"
+# define TZDEFRULES "posixrules"
 #endif /* !defined TZDEFRULES */
 
 
@@ -44,7 +36,7 @@ 
 
 struct tzhead {
 	char	tzh_magic[4];		/* TZ_MAGIC */
-	char	tzh_version[1];		/* '\0' or '2' or '3' as of 2013 */
+	char	tzh_version[1];		/* '\0' or '2'-'4' as of 2021 */
 	char	tzh_reserved[15];	/* reserved; must be zero */
 	char	tzh_ttisutcnt[4];	/* coded number of trans. time flags */
 	char	tzh_ttisstdcnt[4];	/* coded number of trans. time flags */
@@ -86,11 +78,11 @@  struct tzhead {
 ** time uses 8 rather than 4 chars,
 ** then a POSIX-TZ-environment-variable-style string for use in handling
 ** instants after the last transition time stored in the file
-** (with nothing between the newlines if there is no POSIX representation for
-** such instants).
+** (with nothing between the newlines if there is no POSIX.1-2017
+** representation for such instants).
 **
 ** If tz_version is '3' or greater, the above is extended as follows.
-** First, the POSIX TZ string's hour offset may range from -167
+** First, the TZ string's hour offset may range from -167
 ** through 167 as compared to the POSIX-required 0 through 24.
 ** Second, its DST start time may be January 1 at 00:00 and its stop
 ** time December 31 at 24:00 plus the difference between DST and
@@ -103,21 +95,25 @@  struct tzhead {
 */
 
 #ifndef TZ_MAX_TIMES
-#define TZ_MAX_TIMES	2000
+/* This must be at least 242 for Europe/London with 'zic -b fat'.  */
+# define TZ_MAX_TIMES 2000
 #endif /* !defined TZ_MAX_TIMES */
 
 #ifndef TZ_MAX_TYPES
-/* This must be at least 17 for Europe/Samara and Europe/Vilnius.  */
-#define TZ_MAX_TYPES	256 /* Limited by what (unsigned char)'s can hold */
+/* This must be at least 18 for Europe/Vilnius with 'zic -b fat'.  */
+# define TZ_MAX_TYPES 256 /* Limited by what (unsigned char)'s can hold */
 #endif /* !defined TZ_MAX_TYPES */
 
 #ifndef TZ_MAX_CHARS
-#define TZ_MAX_CHARS	50	/* Maximum number of abbreviation characters */
+/* This must be at least 40 for America/Anchorage.  */
+# define TZ_MAX_CHARS 50	/* Maximum number of abbreviation characters */
 				/* (limited by what unsigned chars can hold) */
 #endif /* !defined TZ_MAX_CHARS */
 
 #ifndef TZ_MAX_LEAPS
-#define TZ_MAX_LEAPS	50	/* Maximum number of leap second corrections */
+/* This must be at least 27 for leap seconds from 1972 through mid-2023.
+   There's a plan to discontinue leap seconds by 2035.  */
+# define TZ_MAX_LEAPS 50	/* Maximum number of leap second corrections */
 #endif /* !defined TZ_MAX_LEAPS */
 
 #endif /* !defined TZFILE_H */
diff --git a/timezone/tzselect.ksh b/timezone/tzselect.ksh
index 18fce27e24..38941bbc55 100755
--- a/timezone/tzselect.ksh
+++ b/timezone/tzselect.ksh
@@ -10,7 +10,7 @@  REPORT_BUGS_TO=tz@iana.org
 
 # Porting notes:
 #
-# This script requires a Posix-like shell and prefers the extension of a
+# This script requires a POSIX-like shell and prefers the extension of a
 # 'select' statement.  The 'select' statement was introduced in the
 # Korn shell and is available in Bash and other shell implementations.
 # If your host lacks both Bash and the Korn shell, you can get their
@@ -18,35 +18,47 @@  REPORT_BUGS_TO=tz@iana.org
 #
 #	Bash <https://www.gnu.org/software/bash/>
 #	Korn Shell <http://www.kornshell.com/>
-#	MirBSD Korn Shell <https://www.mirbsd.org/mksh.htm>
+#	MirBSD Korn Shell <http://www.mirbsd.org/mksh.htm>
 #
-# For portability to Solaris 9 /bin/sh this script avoids some POSIX
-# features and common extensions, such as $(...) (which works sometimes
-# but not others), $((...)), and $10.
+# For portability to Solaris 10 /bin/sh (supported by Oracle through
+# January 2027) this script avoids some POSIX features and common
+# extensions, such as $(...), $((...)), ! CMD, unquoted ^, ${#ID},
+# ${ID##PAT}, ${ID%%PAT}, and $10.  Although some of these constructs
+# work sometimes, it's simpler to avoid them entirely.
 #
-# This script also uses several features of modern awk programs.
-# If your host lacks awk, or has an old awk that does not conform to Posix,
-# you can use either of the following free programs instead:
+# This script also uses several features of POSIX awk.
+# If your host lacks awk, or has an old awk that does not conform to POSIX,
+# you can use any of the following free programs instead:
 #
 #	Gawk (GNU awk) <https://www.gnu.org/software/gawk/>
 #	mawk <https://invisible-island.net/mawk/>
+#	nawk <https://github.com/onetrueawk/awk>
+#
+# Because 'awk "VAR=VALUE" ...' and 'awk -v "VAR=VALUE" ...' are not portable
+# if VALUE contains \, ", or newline, awk scripts in this file use:
+#   awk 'BEGIN { VAR = substr(ARGV[1], 2); ARGV[1] = "" } ...' ="VALUE"
+# The substr avoids problems when VALUE is of the form X=Y and would be
+# misinterpreted as an assignment.
 
+# This script does not want path expansion.
+set -f
 
 # Specify default values for environment variables if they are unset.
 : ${AWK=awk}
-: ${TZDIR=`pwd`}
+: ${PWD=`pwd`}
+: ${TZDIR=$PWD}
 
-# Output one argument as-is to standard output.
+# Output one argument as-is to standard output, with trailing newline.
 # Safer than 'echo', which can mishandle '\' or leading '-'.
 say() {
-    printf '%s\n' "$1"
+  printf '%s\n' "$1"
 }
 
-# Check for awk Posix compliance.
-($AWK -v x=y 'BEGIN { exit 123 }') </dev/null >/dev/null 2>&1
+# Check for awk POSIX compliance.
+($AWK -v x=y 'BEGIN { exit 123 }') <>/dev/null >&0 2>&0
 [ $? = 123 ] || {
-	say >&2 "$0: Sorry, your '$AWK' program is not Posix compatible."
-	exit 1
+  say >&2 "$0: Sorry, your '$AWK' program is not POSIX compatible."
+  exit 1
 }
 
 coord=
@@ -79,14 +91,14 @@  Report bugs to $REPORT_BUGS_TO."
 
 # Ask the user to select from the function's arguments,
 # and assign the selected argument to the variable 'select_result'.
-# Exit on EOF or I/O error.  Use the shell's 'select' builtin if available,
-# falling back on a less-nice but portable substitute otherwise.
+# Exit on EOF or I/O error.  Use the shell's nicer 'select' builtin if
+# available, falling back on a portable substitute otherwise.
 if
   case $BASH_VERSION in
-  ?*) : ;;
+  ?*) :;;
   '')
     # '; exit' should be redundant, but Dash doesn't properly fail without it.
-    (eval 'set --; select x; do break; done; exit') </dev/null 2>/dev/null
+    (eval 'set --; select x; do break; done; exit') <>/dev/null 2>&0
   esac
 then
   # Do this inside 'eval', as otherwise the shell might exit when parsing it
@@ -96,24 +108,17 @@  then
       select select_result
       do
 	case $select_result in
-	"") echo >&2 "Please enter a number in range." ;;
+	"") echo >&2 "Please enter a number in range.";;
 	?*) break
 	esac
       done || exit
     }
-
-    # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
-    case $BASH_VERSION in
-    [01].*)
-      case `echo 1 | (select x in x; do break; done) 2>/dev/null` in
-      ?*) PS3=
-      esac
-    esac
   '
 else
   doselect() {
     # Field width of the prompt numbers.
-    select_width=`expr $# : '.*'`
+    print_nargs_length="BEGIN {print length(\"$#\");}"
+    select_width=`$AWK "$print_nargs_length"`
 
     select_i=
 
@@ -124,14 +129,14 @@  else
 	select_i=0
 	for select_word
 	do
-	  select_i=`expr $select_i + 1`
+	  select_i=`$AWK "BEGIN { print $select_i + 1 }"`
 	  printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
-	done ;;
+	done;;
       *[!0-9]*)
-	echo >&2 'Please enter a number in range.' ;;
+	echo >&2 'Please enter a number in range.';;
       *)
 	if test 1 -le $select_i && test $select_i -le $#; then
-	  shift `expr $select_i - 1`
+	  shift `$AWK "BEGIN { print $select_i - 1 }"`
 	  select_result=$1
 	  break
 	fi
@@ -147,70 +152,137 @@  fi
 
 while getopts c:n:t:-: opt
 do
-    case $opt$OPTARG in
-    c*)
-	coord=$OPTARG ;;
-    n*)
-	location_limit=$OPTARG ;;
-    t*) # Undocumented option, used for developer testing.
-	zonetabtype=$OPTARG ;;
-    -help)
-	exec echo "$usage" ;;
-    -version)
-	exec echo "tzselect $PKGVERSION$TZVERSION" ;;
-    -*)
-	say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
-    *)
-	say >&2 "$0: try '$0 --help'"; exit 1 ;;
-    esac
+  case $opt$OPTARG in
+  c*)
+    coord=$OPTARG;;
+  n*)
+    location_limit=$OPTARG;;
+  t*) # Undocumented option, used for developer testing.
+    zonetabtype=$OPTARG;;
+  -help)
+    exec echo "$usage";;
+  -version)
+    exec echo "tzselect $PKGVERSION$TZVERSION";;
+  -*)
+    say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1;;
+  *)
+    say >&2 "$0: try '$0 --help'"; exit 1
+  esac
 done
 
-shift `expr $OPTIND - 1`
+shift `$AWK "BEGIN { print $OPTIND - 1 }"`
 case $# in
 0) ;;
-*) say >&2 "$0: $1: unknown argument"; exit 1 ;;
+*) say >&2 "$0: $1: unknown argument"; exit 1
 esac
 
-# Make sure the tables are readable.
-TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
-TZ_ZONE_TABLE=$TZDIR/$zonetabtype.tab
-for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
-do
-	<"$f" || {
-		say >&2 "$0: time zone files are not set up correctly"
-		exit 1
-	}
-done
+# translit=true to try transliteration.
+# This is false if U+12345 CUNEIFORM SIGN URU TIMES KI has length 1
+# which means awk (and presumably the shell) do not need transliteration.
+if $AWK 'BEGIN { u12345 = "\360\222\215\205"; exit length(u12345) == 1 }'; then
+  translit=true
+else
+  translit=false
+fi
 
-# If the current locale does not support UTF-8, convert data to current
-# locale's format if possible, as the shell aligns columns better that way.
-# Check the UTF-8 of U+12345 CUNEIFORM SIGN URU TIMES KI.
-! $AWK 'BEGIN { u12345 = "\360\222\215\205"; exit length(u12345) != 1 }' &&
-    { tmp=`(mktemp -d) 2>/dev/null` || {
-	tmp=${TMPDIR-/tmp}/tzselect.$$ &&
-	(umask 77 && mkdir -- "$tmp")
-    };} &&
-    trap 'status=$?; rm -fr -- "$tmp"; exit $status' 0 HUP INT PIPE TERM &&
-    (iconv -f UTF-8 -t //TRANSLIT <"$TZ_COUNTRY_TABLE" >$tmp/iso3166.tab) \
-        2>/dev/null &&
-    TZ_COUNTRY_TABLE=$tmp/iso3166.tab &&
-    iconv -f UTF-8 -t //TRANSLIT <"$TZ_ZONE_TABLE" >$tmp/$zonetabtype.tab &&
-    TZ_ZONE_TABLE=$tmp/$zonetabtype.tab
+# Read into shell variable $1 the contents of file $2.
+# Convert to the current locale's encoding if possible,
+# as the shell aligns columns better that way.
+# If GNU iconv's //TRANSLIT does not work, fall back on POSIXish iconv;
+# if that does not work, fall back on 'cat'.
+read_file() {
+  { $translit && {
+    eval "$1=\`(iconv -f UTF-8 -t //TRANSLIT) 2>/dev/null <\"\$2\"\`" ||
+    eval "$1=\`(iconv -f UTF-8) 2>/dev/null <\"\$2\"\`"
+  }; } ||
+  eval "$1=\`cat <\"\$2\"\`" || {
+    say >&2 "$0: time zone files are not set up correctly"
+    exit 1
+  }
+}
+read_file TZ_COUNTRY_TABLE "$TZDIR/iso3166.tab"
+read_file TZ_ZONETABTYPE_TABLE "$TZDIR/$zonetabtype.tab"
+TZ_ZONENOW_TABLE=
 
 newline='
 '
 IFS=$newline
 
+# Awk script to output a country list.
+output_country_list='
+  BEGIN {
+    continent_re = substr(ARGV[1], 2)
+    TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
+    TZ_ZONE_TABLE = substr(ARGV[3], 2)
+    ARGV[1] = ARGV[2] = ARGV[3] = ""
+    FS = "\t"
+    nlines = split(TZ_ZONE_TABLE, line, /\n/)
+    for (iline = 1; iline <= nlines; iline++) {
+      $0 = line[iline]
+      commentary = $0 ~ /^#@/
+      if (commentary) {
+	if ($0 !~ /^#@/)
+	  continue
+	col1ccs = substr($1, 3)
+	conts = $2
+      } else {
+	col1ccs = $1
+	conts = $3
+      }
+      ncc = split(col1ccs, cc, /,/)
+      ncont = split(conts, cont, /,/)
+      for (i = 1; i <= ncc; i++) {
+	elsewhere = commentary
+	for (ci = 1; ci <= ncont; ci++) {
+	  if (cont[ci] ~ continent_re) {
+	    if (!cc_seen[cc[i]]++)
+	      cc_list[++ccs] = cc[i]
+	    elsewhere = 0
+	  }
+	}
+	if (elsewhere)
+	  for (i = 1; i <= ncc; i++)
+	    cc_elsewhere[cc[i]] = 1
+      }
+    }
+    nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
+    for (i = 1; i <= nlines; i++) {
+      $0 = line[i]
+      if ($0 !~ /^#/)
+	cc_name[$1] = $2
+    }
+    for (i = 1; i <= ccs; i++) {
+      country = cc_list[i]
+      if (cc_elsewhere[country])
+	continue
+      if (cc_name[country])
+	country = cc_name[country]
+      print country
+    }
+  }
+'
 
-# Awk script to read a time zone table and output the same table,
-# with each column preceded by its distance from 'here'.
-output_distances='
+# Awk script to process a time zone table and output the same table,
+# with each row preceded by its distance from 'here'.
+# If output_times is set, each row is instead preceded by its local time
+# and any apostrophes are escaped for the shell.
+output_distances_or_times='
   BEGIN {
+    coord = substr(ARGV[1], 2)
+    TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
+    TZ_ZONE_TABLE = substr(ARGV[3], 2)
+    ARGV[1] = ARGV[2] = ARGV[3] = ""
     FS = "\t"
-    while (getline <TZ_COUNTRY_TABLE)
-      if ($0 ~ /^[^#]/)
-        country[$1] = $2
-    country["US"] = "US" # Otherwise the strings get too long.
+    if (!output_times) {
+      nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
+      for (i = 1; i <= nlines; i++) {
+	$0 = line[i]
+	if ($0 ~ /^#/)
+	  continue
+	country[$1] = $2
+      }
+      country["US"] = "US" # Otherwise the strings get too long.
+    }
   }
   function abs(x) {
     return x < 0 ? -x : x;
@@ -270,281 +342,466 @@  output_distances='
   BEGIN {
     coord_lat = convert_latitude(coord)
     coord_long = convert_longitude(coord)
-  }
-  /^[^#]/ {
-    here_lat = convert_latitude($2)
-    here_long = convert_longitude($2)
-    line = $1 "\t" $2 "\t" $3
-    sep = "\t"
-    ncc = split($1, cc, /,/)
-    for (i = 1; i <= ncc; i++) {
-      line = line sep country[cc[i]]
-      sep = ", "
+    nlines = split(TZ_ZONE_TABLE, line, /\n/)
+    for (h = 1; h <= nlines; h++) {
+      $0 = line[h]
+      if ($0 ~ /^#/)
+	continue
+      inline[inlines++] = $0
+      ncc = split($1, cc, /,/)
+      for (i = 1; i <= ncc; i++)
+	cc_used[cc[i]]++
+    }
+    for (h = 0; h < inlines; h++) {
+      $0 = inline[h]
+      outline = $1 "\t" $2 "\t" $3
+      sep = "\t"
+      ncc = split($1, cc, /,/)
+      split("", item_seen)
+      item_seen[""] = 1
+      for (i = 1; i <= ncc; i++) {
+	item = cc_used[cc[i]] <= 1 ? country[cc[i]] : $4
+	if (item_seen[item]++)
+	  continue
+	outline = outline sep item
+	sep = "; "
+      }
+      if (output_times) {
+	fmt = "TZ='\''%s'\'' date +'\''%d %%Y %%m %%d %%H:%%M %%a %%b\t%s'\''\n"
+	gsub(/'\''/, "&\\\\&&", outline)
+	printf fmt, $3, h, outline
+      } else {
+	here_lat = convert_latitude($2)
+	here_long = convert_longitude($2)
+	printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), \
+	  outline
+      }
     }
-    if (NF == 4)
-      line = line " - " $4
-    printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
   }
 '
 
 # Begin the main loop.  We come back here if the user wants to retry.
 while
 
-	echo >&2 'Please identify a location' \
-		'so that time zone rules can be set correctly.'
+  echo >&2 'Please identify a location' \
+    'so that time zone rules can be set correctly.'
 
-	continent=
-	country=
-	region=
+  continent=
+  country=
+  country_result=
+  region=
+  time=
+  TZ_ZONE_TABLE=$TZ_ZONETABTYPE_TABLE
 
-	case $coord in
-	?*)
-		continent=coord;;
-	'')
+  case $coord in
+  ?*)
+    continent=coord;;
+  '')
 
-	# Ask the user for continent or ocean.
+    # Ask the user for continent or ocean.
 
-	echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
+    echo >&2 \
+      'Please select a continent, ocean, "coord", "TZ", "time", or "now".'
 
-        quoted_continents=`
+    quoted_continents=`
+      $AWK '
+	function handle_entry(entry) {
+	  entry = substr(entry, 1, index(entry, "/") - 1)
+	  if (entry == "America")
+	    entry = entry "s"
+	  if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
+	    entry = entry " Ocean"
+	  printf "'\''%s'\''\n", entry
+	}
+	BEGIN {
+	  TZ_ZONETABTYPE_TABLE = substr(ARGV[1], 2)
+	  ARGV[1] = ""
+	  FS = "\t"
+	  nlines = split(TZ_ZONETABTYPE_TABLE, line, /\n/)
+	  for (i = 1; i <= nlines; i++) {
+	    $0 = line[i]
+	    if ($0 ~ /^[^#]/)
+	      handle_entry($3)
+	    else if ($0 ~ /^#@/) {
+	      ncont = split($2, cont, /,/)
+	      for (ci = 1; ci <= ncont; ci++)
+		handle_entry(cont[ci])
+	    }
+	  }
+	}
+      ' ="$TZ_ZONETABTYPE_TABLE" |
+      sort -u |
+      tr '\n' ' '
+      echo ''
+    `
+
+    eval '
+      doselect '"$quoted_continents"' \
+	"coord - I want to use geographical coordinates." \
+	"TZ - I want to specify the timezone using a POSIX.1-2017 TZ string." \
+	"time - I know local time already." \
+	"now - Like \"time\", but configure only for timestamps from now on."
+      continent=$select_result
+      case $continent in
+      Americas) continent=America;;
+      *)
+	# Get the first word of $continent.  Path expansion is disabled
+	# so this works even with "*", which should not happen.
+	IFS=" "
+	for continent in $continent ""; do break; done
+	IFS=$newline;;
+      esac
+      case $zonetabtype,$continent in
+      zonenow,*) ;;
+      *,now)
+	${TZ_ZONENOW_TABLE:+:} read_file TZ_ZONENOW_TABLE "$TZDIR/zonenow.tab"
+	TZ_ZONE_TABLE=$TZ_ZONENOW_TABLE
+      esac
+    '
+  esac
+
+  case $continent in
+  TZ)
+    # Ask the user for a POSIX.1-2017 TZ string.  Check that it conforms.
+    check_POSIX_TZ_string='
+      BEGIN {
+	tz = substr(ARGV[1], 2)
+	ARGV[1] = ""
+	tzname = ("(<[[:alnum:]+-][[:alnum:]+-][[:alnum:]+-]+>" \
+		  "|[[:alpha:]][[:alpha:]][[:alpha:]]+)")
+	time = ("(2[0-4]|[0-1]?[0-9])" \
+		"(:[0-5][0-9](:[0-5][0-9])?)?")
+	offset = "[-+]?" time
+	mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]"
+	jdate = ("((J[1-9]|[0-9]|J?[1-9][0-9]" \
+		 "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])")
+	datetime = ",(" mdate "|" jdate ")(/" time ")?"
+	tzpattern = ("^(:.*|" tzname offset "(" tzname \
+		     "(" offset ")?(" datetime datetime ")?)?)$")
+	exit tz ~ tzpattern
+      }
+    '
+
+    while
+      echo >&2 'Please enter the desired value' \
+	'of the TZ environment variable.'
+      echo >&2 'For example, AEST-10 is abbreviated' \
+	'AEST and is 10 hours'
+      echo >&2 'ahead (east) of Greenwich,' \
+	'with no daylight saving time.'
+      read tz
+      $AWK "$check_POSIX_TZ_string" ="$tz"
+    do
+      say >&2 "'$tz' is not a conforming POSIX.1-2017 timezone string."
+    done
+    TZ_for_date=$tz;;
+  *)
+    case $continent in
+    coord)
+      case $coord in
+      '')
+	echo >&2 'Please enter coordinates' \
+	  'in ISO 6709 notation.'
+	echo >&2 'For example, +4042-07403 stands for'
+	echo >&2 '40 degrees 42 minutes north,' \
+	  '74 degrees 3 minutes west.'
+	read coord
+      esac
+      distance_table=`
+	$AWK \
+	  "$output_distances_or_times" \
+	  ="$coord" ="$TZ_COUNTRY_TABLE" ="$TZ_ZONE_TABLE" |
+	sort -n |
+	$AWK "{print} NR == $location_limit { exit }"
+      `
+      regions=`
+	$AWK '
+	  BEGIN {
+	    distance_table = substr(ARGV[1], 2)
+	    ARGV[1] = ""
+	    nlines = split(distance_table, line, /\n/)
+	    for (nr = 1; nr <= nlines; nr++) {
+	      nf = split(line[nr], f, /\t/)
+	      print f[nf]
+	    }
+	  }
+	' ="$distance_table"
+      `
+      echo >&2 'Please select one of the following timezones,'
+      echo >&2 'listed roughly in increasing order' \
+	"of distance from $coord".
+      doselect $regions
+      region=$select_result
+      tz=`
+	$AWK '
+	  BEGIN {
+	    distance_table = substr(ARGV[1], 2)
+	    region = substr(ARGV[2], 2)
+	    ARGV[1] = ARGV[2] = ""
+	    nlines = split(distance_table, line, /\n/)
+	    for (nr = 1; nr <= nlines; nr++) {
+	      nf = split(line[nr], f, /\t/)
+	      if (f[nf] == region)
+		print f[4]
+	    }
+	  }
+	' ="$distance_table" ="$region"
+      `;;
+    *)
+      case $continent in
+      now|time)
+	minute_format='%a %b %d %H:%M'
+	old_minute=`TZ=UTC0 date +"$minute_format"`
+	for i in 1 2 3
+	do
+	  time_table_command=`
+	    $AWK \
+	      -v output_times=1 \
+	      "$output_distances_or_times" \
+	      = = ="$TZ_ZONE_TABLE"
+	  `
+	  time_table=`eval "$time_table_command"`
+	  new_minute=`TZ=UTC0 date +"$minute_format"`
+	  case $old_minute in
+	  "$new_minute") break
+	  esac
+	  old_minute=$new_minute
+	done
+	echo >&2 "The system says Universal Time is $new_minute."
+	echo >&2 "Assuming that's correct, what is the local time?"
+	sorted_table=`say "$time_table" | sort -k2n -k2,5 -k1n` || {
+	  say >&2 "$0: cannot sort time table"
+	  exit 1
+	}
+	eval doselect `
 	  $AWK '
-	    BEGIN { FS = "\t" }
-	    /^[^#]/ {
-              entry = substr($3, 1, index($3, "/") - 1)
-              if (entry == "America")
-		entry = entry "s"
-              if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
-		entry = entry " Ocean"
-              printf "'\''%s'\''\n", entry
-            }
-          ' <"$TZ_ZONE_TABLE" |
-	  sort -u |
-	  tr '\n' ' '
-	  echo ''
+	    BEGIN {
+	      sorted_table = substr(ARGV[1], 2)
+	      ARGV[1] = ""
+	      nlines = split(sorted_table, line, /\n/)
+	      for (i = 1; i <= nlines; i++) {
+		$0 = line[i]
+		outline = $6 " " $7 " " $4 " " $5
+		if (outline == oldline)
+		  continue
+		oldline = outline
+		gsub(/'\''/, "&\\\\&&", outline)
+		printf "'\''%s'\''\n", outline
+	      }
+	    }
+	  ' ="$sorted_table"
 	`
-
-	eval '
-	    doselect '"$quoted_continents"' \
-		"coord - I want to use geographical coordinates." \
-		"TZ - I want to specify the timezone using the Posix TZ format."
-	    continent=$select_result
-	    case $continent in
-	    Americas) continent=America;;
-	    *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''`
-	    esac
-	'
-	esac
-
-	case $continent in
-	TZ)
-		# Ask the user for a Posix TZ string.  Check that it conforms.
-		while
-			echo >&2 'Please enter the desired value' \
-				'of the TZ environment variable.'
-			echo >&2 'For example, AEST-10 is abbreviated' \
-				'AEST and is 10 hours'
-			echo >&2 'ahead (east) of Greenwich,' \
-				'with no daylight saving time.'
-			read TZ
-			$AWK -v TZ="$TZ" 'BEGIN {
-				tzname = "(<[[:alnum:]+-]{3,}>|[[:alpha:]]{3,})"
-				time = "(2[0-4]|[0-1]?[0-9])" \
-				  "(:[0-5][0-9](:[0-5][0-9])?)?"
-				offset = "[-+]?" time
-				mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]"
-				jdate = "((J[1-9]|[0-9]|J?[1-9][0-9]" \
-				  "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])"
-				datetime = ",(" mdate "|" jdate ")(/" time ")?"
-				tzpattern = "^(:.*|" tzname offset "(" tzname \
-				  "(" offset ")?(" datetime datetime ")?)?)$"
-				if (TZ ~ tzpattern) exit 1
-				exit 0
-			}'
-		do
-		    say >&2 "'$TZ' is not a conforming Posix timezone string."
-		done
-		TZ_for_date=$TZ;;
-	*)
-		case $continent in
-		coord)
-		    case $coord in
-		    '')
-			echo >&2 'Please enter coordinates' \
-				'in ISO 6709 notation.'
-			echo >&2 'For example, +4042-07403 stands for'
-			echo >&2 '40 degrees 42 minutes north,' \
-				'74 degrees 3 minutes west.'
-			read coord;;
-		    esac
-		    distance_table=`$AWK \
-			    -v coord="$coord" \
-			    -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
-			    "$output_distances" <"$TZ_ZONE_TABLE" |
-		      sort -n |
-		      sed "${location_limit}q"
-		    `
-		    regions=`say "$distance_table" | $AWK '
-		      BEGIN { FS = "\t" }
-		      { print $NF }
-		    '`
-		    echo >&2 'Please select one of the following timezones,' \
-		    echo >&2 'listed roughly in increasing order' \
-			    "of distance from $coord".
-		    doselect $regions
-		    region=$select_result
-		    TZ=`say "$distance_table" | $AWK -v region="$region" '
-		      BEGIN { FS="\t" }
-		      $NF == region { print $4 }
-		    '`
-		    ;;
-		*)
-		# Get list of names of countries in the continent or ocean.
-		countries=`$AWK \
-			-v continent="$continent" \
-			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
-		'
-			BEGIN { FS = "\t" }
-			/^#/ { next }
-			$3 ~ ("^" continent "/") {
-			    ncc = split($1, cc, /,/)
-			    for (i = 1; i <= ncc; i++)
-				if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i]
-			}
-			END {
-				while (getline <TZ_COUNTRY_TABLE) {
-					if ($0 !~ /^#/) cc_name[$1] = $2
-				}
-				for (i = 1; i <= ccs; i++) {
-					country = cc_list[i]
-					if (cc_name[country]) {
-					  country = cc_name[country]
-					}
-					print country
-				}
-			}
-		' <"$TZ_ZONE_TABLE" | sort -f`
-
-
-		# If there's more than one country, ask the user which one.
-		case $countries in
-		*"$newline"*)
-			echo >&2 'Please select a country' \
-				'whose clocks agree with yours.'
-			doselect $countries
-			country=$select_result;;
-		*)
-			country=$countries
-		esac
-
-
-		# Get list of timezones in the country.
-		regions=`$AWK \
-			-v country="$country" \
-			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
-		'
-			BEGIN {
-				FS = "\t"
-				cc = country
-				while (getline <TZ_COUNTRY_TABLE) {
-					if ($0 !~ /^#/  &&  country == $2) {
-						cc = $1
-						break
-					}
-				}
-			}
-			/^#/ { next }
-			$1 ~ cc { print $4 }
-		' <"$TZ_ZONE_TABLE"`
-
-
-		# If there's more than one region, ask the user which one.
-		case $regions in
-		*"$newline"*)
-			echo >&2 'Please select one of the following timezones.'
-			doselect $regions
-			region=$select_result;;
-		*)
-			region=$regions
-		esac
-
-		# Determine TZ from country and region.
-		TZ=`$AWK \
-			-v country="$country" \
-			-v region="$region" \
-			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
-		'
-			BEGIN {
-				FS = "\t"
-				cc = country
-				while (getline <TZ_COUNTRY_TABLE) {
-					if ($0 !~ /^#/  &&  country == $2) {
-						cc = $1
-						break
-					}
-				}
-			}
-			/^#/ { next }
-			$1 ~ cc && $4 == region { print $3 }
-		' <"$TZ_ZONE_TABLE"`
-		esac
-
-		# Make sure the corresponding zoneinfo file exists.
-		TZ_for_date=$TZDIR/$TZ
-		<"$TZ_for_date" || {
-			say >&2 "$0: time zone files are not set up correctly"
-			exit 1
+	time=$select_result
+	continent_re='^'
+	zone_table=`
+	  $AWK '
+	    BEGIN {
+	      time = substr(ARGV[1], 2)
+	      time_table = substr(ARGV[2], 2)
+	      ARGV[1] = ARGV[2] = ""
+	      nlines = split(time_table, line, /\n/)
+	      for (i = 1; i <= nlines; i++) {
+		$0 = line[i]
+		if ($6 " " $7 " " $4 " " $5 == time) {
+		  sub(/[^\t]*\t/, "")
+		  print
 		}
-	esac
+	      }
+	    }
+	  ' ="$time" ="$time_table"
+	`
+	countries=`
+	  $AWK \
+	    "$output_country_list" \
+	    ="$continent_re" ="$TZ_COUNTRY_TABLE" ="$zone_table" |
+	  sort -f
+	`
+	;;
+      *)
+	continent_re="^$continent/"
+	zone_table=$TZ_ZONE_TABLE
+      esac
 
+      # Get list of names of countries in the continent or ocean.
+      countries=`
+	$AWK \
+	  "$output_country_list" \
+	  ="$continent_re" ="$TZ_COUNTRY_TABLE" ="$zone_table" |
+	sort -f
+      `
+      # If all zone table entries have comments, and there are
+      # at most 22 entries, asked based on those comments.
+      # This fits the prompt onto old-fashioned 24-line screens.
+      regions=`
+	$AWK '
+	  BEGIN {
+	    TZ_ZONE_TABLE = substr(ARGV[1], 2)
+	    ARGV[1] = ""
+	    FS = "\t"
+	    nlines = split(TZ_ZONE_TABLE, line, /\n/)
+	    for (i = 1; i <= nlines; i++) {
+	      $0 = line[i]
+	      if ($0 ~ /^[^#]/ && !missing_comment) {
+		if ($4)
+		  comment[++inlines] = $4
+		else
+		  missing_comment = 1
+	      }
+	    }
+	    if (!missing_comment && inlines <= 22)
+	      for (i = 1; i <= inlines; i++)
+		print comment[i]
+	  }
+	' ="$zone_table"
+      `
+
+      # If there's more than one country, ask the user which one.
+      case $countries in
+      *"$newline"*)
+	echo >&2 'Please select a country' \
+	  'whose clocks agree with yours.'
+	doselect $countries
+	country_result=$select_result
+	country=$select_result;;
+      *)
+	country=$countries
+      esac
 
-	# Use the proposed TZ to output the current date relative to UTC.
-	# Loop until they agree in seconds.
-	# Give up after 8 unsuccessful tries.
 
-	extra_info=
-	for i in 1 2 3 4 5 6 7 8
-	do
-		TZdate=`LANG=C TZ="$TZ_for_date" date`
-		UTdate=`LANG=C TZ=UTC0 date`
-		TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
-		UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
-		case $TZsec in
-		$UTsec)
-			extra_info="
-Selected time is now:	$TZdate.
-Universal Time is now:	$UTdate."
-			break
-		esac
-	done
+      # Get list of timezones in the country.
+      regions=`
+	$AWK '
+	  BEGIN {
+	    country = substr(ARGV[1], 2)
+	    TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
+	    TZ_ZONE_TABLE = substr(ARGV[3], 2)
+	    ARGV[1] = ARGV[2] = ARGV[3] = ""
+	    FS = "\t"
+	    cc = country
+	    nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
+	    for (i = 1; i <= nlines; i++) {
+	      $0 = line[i]
+	      if ($0 !~ /^#/  &&  country == $2) {
+		cc = $1
+		break
+	      }
+	    }
+	    nlines = split(TZ_ZONE_TABLE, line, /\n/)
+	    for (i = 1; i <= nlines; i++) {
+	      $0 = line[i]
+	      if ($0 ~ /^#/)
+		continue
+	      if ($1 ~ cc)
+		print $4
+	    }
+	  }
+	' ="$country" ="$TZ_COUNTRY_TABLE" ="$zone_table"
+      `
+
+      # If there's more than one region, ask the user which one.
+      case $regions in
+      *"$newline"*)
+	echo >&2 'Please select one of the following timezones.'
+	doselect $regions
+	region=$select_result
+      esac
+
+      # Determine tz from country and region.
+      tz=`
+	$AWK '
+	  BEGIN {
+	    country = substr(ARGV[1], 2)
+	    region = substr(ARGV[2], 2)
+	    TZ_COUNTRY_TABLE = substr(ARGV[3], 2)
+	    TZ_ZONE_TABLE = substr(ARGV[4], 2)
+	    ARGV[1] = ARGV[2] = ARGV[3] = ARGV[4] = ""
+	    FS = "\t"
+	    cc = country
+	    nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
+	    for (i = 1; i <= nlines; i++) {
+	      $0 = line[i]
+	      if ($0 !~ /^#/  &&  country == $2) {
+		cc = $1
+		break
+	      }
+	    }
+	    nlines = split(TZ_ZONE_TABLE, line, /\n/)
+	    for (i = 1; i <= nlines; i++) {
+	      $0 = line[i]
+	      if ($0 ~ /^#/)
+		continue
+	      if ($1 ~ cc && ($4 == region || !region))
+		print $3
+	    }
+	  }
+	' ="$country" ="$region" ="$TZ_COUNTRY_TABLE" ="$zone_table"
+      `
+    esac
 
+    # Make sure the corresponding zoneinfo file exists.
+    TZ_for_date=$TZDIR/$tz
+    <"$TZ_for_date" || {
+      say >&2 "$0: time zone files are not set up correctly"
+      exit 1
+    }
+  esac
 
-	# Output TZ info and ask the user to confirm.
 
-	echo >&2 ""
-	echo >&2 "The following information has been given:"
-	echo >&2 ""
-	case $country%$region%$coord in
-	?*%?*%)	say >&2 "	$country$newline	$region";;
-	?*%%)	say >&2 "	$country";;
-	%?*%?*) say >&2 "	coord $coord$newline	$region";;
-	%%?*)	say >&2 "	coord $coord";;
-	*)	say >&2 "	TZ='$TZ'"
-	esac
-	say >&2 ""
-	say >&2 "Therefore TZ='$TZ' will be used.$extra_info"
-	say >&2 "Is the above information OK?"
-
-	doselect Yes No
-	ok=$select_result
-	case $ok in
-	Yes) break
-	esac
+  # Use the proposed TZ to output the current date relative to UTC.
+  # Loop until they agree in seconds.
+  # Give up after 8 unsuccessful tries.
+
+  extra_info=
+  for i in 1 2 3 4 5 6 7 8
+  do
+    TZdate=`LANG=C TZ="$TZ_for_date" date`
+    UTdate=`LANG=C TZ=UTC0 date`
+    if $AWK '
+	  function getsecs(d) {
+	    return match(d, /.*:[0-5][0-9]/) ? substr(d, RLENGTH - 1, 2) : ""
+	  }
+	  BEGIN { exit getsecs(ARGV[1]) != getsecs(ARGV[2]) }
+       ' ="$TZdate" ="$UTdate"
+    then
+      extra_info="
+Selected time is now:	$TZdate.
+Universal Time is now:	$UTdate."
+      break
+    fi
+  done
+
+
+  # Output TZ info and ask the user to confirm.
+
+  echo >&2 ""
+  echo >&2 "Based on the following information:"
+  echo >&2 ""
+  case $time%$country_result%$region%$coord in
+  ?*%?*%?*%)
+    say >&2 "	$time$newline	$country_result$newline	$region";;
+  ?*%?*%%|?*%%?*%) say >&2 "	$time$newline	$country_result$region";;
+  ?*%%%)	say >&2 "	$time";;
+  %?*%?*%)	say >&2 "	$country_result$newline	$region";;
+  %?*%%)	say >&2 "	$country_result";;
+  %%?*%?*)	say >&2 "	coord $coord$newline	$region";;
+  %%%?*)	say >&2 "	coord $coord";;
+  *)		say >&2 "	TZ='$tz'"
+  esac
+  say >&2 ""
+  say >&2 "TZ='$tz' will be used.$extra_info"
+  say >&2 "Is the above information OK?"
+
+  doselect Yes No
+  ok=$select_result
+  case $ok in
+  Yes) break
+  esac
 do coord=
 done
 
 case $SHELL in
-*csh) file=.login line="setenv TZ '$TZ'";;
-*) file=.profile line="TZ='$TZ'; export TZ"
+*csh) file=.login line="setenv TZ '$tz'";;
+*)    file=.profile line="TZ='$tz'; export TZ"
 esac
 
 test -t 1 && say >&2 "
@@ -555,4 +812,4 @@  to the file '$file' in your home directory; then log out and log in again.
 Here is that TZ value again, this time on standard output so that you
 can use the $0 command in shell scripts:"
 
-say "$TZ"
+say "$tz"
diff --git a/timezone/version b/timezone/version
index 7f680eec36..04fe674443 100644
--- a/timezone/version
+++ b/timezone/version
@@ -1 +1 @@ 
-2020a
+2024a
diff --git a/timezone/zdump.c b/timezone/zdump.c
index b532fe3eae..7d99cc74bd 100644
--- a/timezone/zdump.c
+++ b/timezone/zdump.c
@@ -15,7 +15,7 @@ 
 #include <stdio.h>
 
 #ifndef HAVE_SNPRINTF
-# define HAVE_SNPRINTF (199901 <= __STDC_VERSION__)
+# define HAVE_SNPRINTF (!PORT_TO_C89 || 199901 <= __STDC_VERSION__)
 #endif
 
 #ifndef HAVE_LOCALTIME_R
@@ -35,17 +35,13 @@ 
 #endif
 
 #ifndef ZDUMP_LO_YEAR
-#define ZDUMP_LO_YEAR	(-500)
+# define ZDUMP_LO_YEAR (-500)
 #endif /* !defined ZDUMP_LO_YEAR */
 
 #ifndef ZDUMP_HI_YEAR
-#define ZDUMP_HI_YEAR	2500
+# define ZDUMP_HI_YEAR 2500
 #endif /* !defined ZDUMP_HI_YEAR */
 
-#ifndef MAX_STRING_LENGTH
-#define MAX_STRING_LENGTH	1024
-#endif /* !defined MAX_STRING_LENGTH */
-
 #define SECSPERNYEAR	(SECSPERDAY * DAYSPERNYEAR)
 #define SECSPERLYEAR	(SECSPERNYEAR + SECSPERDAY)
 #define SECSPER400YEARS	(SECSPERNYEAR * (intmax_t) (300 + 3)	\
@@ -61,7 +57,7 @@ 
 enum { SECSPER400YEARS_FITS = SECSPERLYEAR <= INTMAX_MAX / 400 };
 
 #if HAVE_GETTEXT
-#include <locale.h>	/* for setlocale */
+# include <locale.h> /* for setlocale */
 #endif /* HAVE_GETTEXT */
 
 #if ! HAVE_LOCALTIME_RZ
@@ -77,7 +73,7 @@  extern int	optind;
 #endif
 
 /* The minimum and maximum finite time values.  */
-enum { atime_shift = CHAR_BIT * sizeof (time_t) - 2 };
+enum { atime_shift = CHAR_BIT * sizeof(time_t) - 2 };
 static time_t const absolute_min_time =
   ((time_t) -1 < 0
    ? (- ((time_t) ~ (time_t) 0 < 0)
@@ -88,22 +84,27 @@  static time_t const absolute_max_time =
    ? (((time_t) 1 << atime_shift) - 1 + ((time_t) 1 << atime_shift))
    : -1);
 static int	longest;
-static char *	progname;
+static char const *progname;
 static bool	warned;
 static bool	errout;
 
 static char const *abbr(struct tm const *);
-static intmax_t	delta(struct tm *, struct tm *) ATTRIBUTE_PURE;
+ATTRIBUTE_REPRODUCIBLE static intmax_t delta(struct tm *, struct tm *);
 static void dumptime(struct tm const *);
-static time_t hunt(timezone_t, char *, time_t, time_t);
+static time_t hunt(timezone_t, time_t, time_t, bool);
 static void show(timezone_t, char *, time_t, bool);
+static void showextrema(timezone_t, char *, time_t, struct tm *, time_t);
 static void showtrans(char const *, struct tm const *, time_t, char const *,
 		      char const *);
 static const char *tformat(void);
-static time_t yeartot(intmax_t) ATTRIBUTE_PURE;
+ATTRIBUTE_REPRODUCIBLE static time_t yeartot(intmax_t);
 
-/* Unlike <ctype.h>'s isdigit, this also works if c < 0 | c > UCHAR_MAX. */
-#define is_digit(c) ((unsigned)(c) - '0' <= 9)
+/* Is C an ASCII digit?  */
+static bool
+is_digit(char c)
+{
+  return '0' <= c && c <= '9';
+}
 
 /* Is A an alphabetic character in the C locale?  */
 static bool
@@ -124,26 +125,49 @@  is_alpha(char a)
 	}
 }
 
-/* Return A + B, exiting if the result would overflow.  */
-static size_t
-sumsize(size_t a, size_t b)
+ATTRIBUTE_NORETURN static void
+size_overflow(void)
 {
-  size_t sum = a + b;
-  if (sum < a) {
-    fprintf(stderr, "%s: size overflow\n", progname);
-    exit(EXIT_FAILURE);
-  }
-  return sum;
+  fprintf(stderr, _("%s: size overflow\n"), progname);
+  exit(EXIT_FAILURE);
+}
+
+/* Return A + B, exiting if the result would overflow either ptrdiff_t
+   or size_t.  A and B are both nonnegative.  */
+ATTRIBUTE_REPRODUCIBLE static ptrdiff_t
+sumsize(ptrdiff_t a, ptrdiff_t b)
+{
+#ifdef ckd_add
+  ptrdiff_t sum;
+  if (!ckd_add(&sum, a, b) && sum <= INDEX_MAX)
+    return sum;
+#else
+  if (a <= INDEX_MAX && b <= INDEX_MAX - a)
+    return a + b;
+#endif
+  size_overflow();
+}
+
+/* Return the size of of the string STR, including its trailing NUL.
+   Report an error and exit if this would exceed INDEX_MAX which means
+   pointer subtraction wouldn't work.  */
+static ptrdiff_t
+xstrsize(char const *str)
+{
+  size_t len = strlen(str);
+  if (len < INDEX_MAX)
+    return len + 1;
+  size_overflow();
 }
 
 /* Return a pointer to a newly allocated buffer of size SIZE, exiting
-   on failure.  SIZE should be nonzero.  */
-static void * ATTRIBUTE_MALLOC
-xmalloc(size_t size)
+   on failure.  SIZE should be positive.  */
+ATTRIBUTE_MALLOC static void *
+xmalloc(ptrdiff_t size)
 {
   void *p = malloc(size);
   if (!p) {
-    perror(progname);
+    fprintf(stderr, _("%s: Memory exhausted\n"), progname);
     exit(EXIT_FAILURE);
   }
   return p;
@@ -204,7 +228,7 @@  localtime_r(time_t *tp, struct tm *tmp)
 # undef localtime_rz
 # define localtime_rz zdump_localtime_rz
 static struct tm *
-localtime_rz(timezone_t rz, time_t *tp, struct tm *tmp)
+localtime_rz(ATTRIBUTE_MAYBE_UNUSED timezone_t rz, time_t *tp, struct tm *tmp)
 {
   return localtime_r(tp, tmp);
 }
@@ -227,33 +251,65 @@  mktime_z(timezone_t tz, struct tm *tmp)
 static timezone_t
 tzalloc(char const *val)
 {
+# if HAVE_SETENV
+  if (setenv("TZ", val, 1) != 0) {
+    char const *e = strerror(errno);
+    fprintf(stderr, _("%s: setenv: %s\n"), progname, e);
+    exit(EXIT_FAILURE);
+  }
+  tzset();
+  return &optarg;  /* Any valid non-null char ** will do.  */
+# else
+  enum { TZeqlen = 3 };
+  static char const TZeq[TZeqlen] = "TZ=";
   static char **fakeenv;
-  char **env = fakeenv;
-  char *env0;
-  if (! env) {
-    char **e = environ;
-    int to;
-
-    while (*e++)
-      continue;
-    env = xmalloc(sumsize(sizeof *environ,
-			  (e - environ) * sizeof *environ));
-    to = 1;
-    for (e = environ; (env[to] = *e); e++)
-      to += strncmp(*e, "TZ=", 3) != 0;
+  static ptrdiff_t fakeenv0size;
+  void *freeable = NULL;
+  char **env = fakeenv, **initial_environ;
+  ptrdiff_t valsize = xstrsize(val);
+  if (fakeenv0size < valsize) {
+    char **e = environ, **to;
+    ptrdiff_t initial_nenvptrs = 1;  /* Counting the trailing NULL pointer.  */
+
+    while (*e++) {
+#  ifdef ckd_add
+      if (ckd_add(&initial_nenvptrs, initial_nenvptrs, 1)
+	  || INDEX_MAX < initial_nenvptrs)
+	size_overflow();
+#  else
+      if (initial_nenvptrs == INDEX_MAX / sizeof *environ)
+	size_overflow();
+      initial_nenvptrs++;
+#  endif
+    }
+    fakeenv0size = sumsize(valsize, valsize);
+    fakeenv0size = max(fakeenv0size, 64);
+    freeable = env;
+    fakeenv = env =
+      xmalloc(sumsize(sumsize(sizeof *environ,
+			      initial_nenvptrs * sizeof *environ),
+		      sumsize(TZeqlen, fakeenv0size)));
+    to = env + 1;
+    for (e = environ; (*to = *e); e++)
+      to += strncmp(*e, TZeq, TZeqlen) != 0;
+    env[0] = memcpy(to + 1, TZeq, TZeqlen);
   }
-  env0 = xmalloc(sumsize(sizeof "TZ=", strlen(val)));
-  env[0] = strcat(strcpy(env0, "TZ="), val);
-  environ = fakeenv = env;
+  memcpy(env[0] + TZeqlen, val, valsize);
+  initial_environ = environ;
+  environ = env;
   tzset();
-  return env;
+  free(freeable);
+  return initial_environ;
+# endif
 }
 
 static void
-tzfree(timezone_t env)
+tzfree(ATTRIBUTE_MAYBE_UNUSED timezone_t initial_environ)
 {
-  environ = env + 1;
-  free(env[0]);
+# if !HAVE_SETENV
+  environ = initial_environ;
+  tzset();
+# endif
 }
 #endif /* ! USE_LOCALTIME_RZ */
 
@@ -263,11 +319,25 @@  static void
 gmtzinit(void)
 {
   if (USE_LOCALTIME_RZ) {
-    static char const utc[] = "UTC0";
-    gmtz = tzalloc(utc);
+    /* Try "GMT" first to find out whether this is one of the rare
+       platforms where time_t counts leap seconds; this works due to
+       the "Zone GMT 0 - GMT" line in the "etcetera" file.  If "GMT"
+       fails, fall back on "GMT0" which might be similar due to the
+       "Link GMT GMT0" line in the "backward" file, and which
+       should work on all POSIX platforms.  The rest of zdump does not
+       use the "GMT" abbreviation that comes from this setting, so it
+       is OK to use "GMT" here rather than the modern "UTC" which
+       would not work on platforms that omit the "backward" file.  */
+    gmtz = tzalloc("GMT");
     if (!gmtz) {
-      perror(utc);
-      exit(EXIT_FAILURE);
+      static char const gmt0[] = "GMT0";
+      gmtz = tzalloc(gmt0);
+      if (!gmtz) {
+	char const *e = strerror(errno);
+	fprintf(stderr, _("%s: unknown timezone '%s': %s\n"),
+		progname, gmt0, e);
+	exit(EXIT_FAILURE);
+      }
     }
   }
 }
@@ -345,23 +415,23 @@  abbrok(const char *const abbrp, const char *const zone)
 
 /* Return a time zone abbreviation.  If the abbreviation needs to be
    saved, use *BUF (of size *BUFALLOC) to save it, and return the
-   abbreviation in the possibly-reallocated *BUF.  Otherwise, just
+   abbreviation in the possibly reallocated *BUF.  Otherwise, just
    return the abbreviation.  Get the abbreviation from TMP.
    Exit on memory allocation failure.  */
 static char const *
-saveabbr(char **buf, size_t *bufalloc, struct tm const *tmp)
+saveabbr(char **buf, ptrdiff_t *bufalloc, struct tm const *tmp)
 {
   char const *ab = abbr(tmp);
   if (HAVE_LOCALTIME_RZ)
     return ab;
   else {
-    size_t ablen = strlen(ab);
-    if (*bufalloc <= ablen) {
+    ptrdiff_t absize = xstrsize(ab);
+    if (*bufalloc < absize) {
       free(*buf);
 
       /* Make the new buffer at least twice as long as the old,
 	 to avoid O(N**2) behavior on repeated calls.  */
-      *bufalloc = sumsize(*bufalloc, ablen + 1);
+      *bufalloc = sumsize(*bufalloc, absize);
 
       *buf = xmalloc(*bufalloc);
     }
@@ -406,7 +476,7 @@  main(int argc, char *argv[])
 {
 	/* These are static so that they're initially zero.  */
 	static char *		abbrev;
-	static size_t		abbrevsize;
+	static ptrdiff_t	abbrevsize;
 
 	register int		i;
 	register bool		vflag;
@@ -422,12 +492,12 @@  main(int argc, char *argv[])
 	cuthitime = absolute_max_time;
 #if HAVE_GETTEXT
 	setlocale(LC_ALL, "");
-#ifdef TZ_DOMAINDIR
+# ifdef TZ_DOMAINDIR
 	bindtextdomain(TZ_DOMAIN, TZ_DOMAINDIR);
-#endif /* defined TEXTDOMAINDIR */
+# endif /* defined TEXTDOMAINDIR */
 	textdomain(TZ_DOMAIN);
 #endif /* HAVE_GETTEXT */
-	progname = argv[0];
+	progname = argv[0] ? argv[0] : "zdump";
 	for (i = 1; i < argc; ++i)
 		if (strcmp(argv[i], "--version") == 0) {
 			printf("zdump %s%s\n", PKGVERSION, TZVERSION);
@@ -447,7 +517,7 @@  main(int argc, char *argv[])
 	  case -1:
 	    if (! (optind == argc - 1 && strcmp(argv[optind], "=") == 0))
 	      goto arg_processing_done;
-	    /* Fall through.  */
+	    ATTRIBUTE_FALLTHROUGH;
 	  default:
 	    usage(stderr, EXIT_FAILURE);
 	  }
@@ -484,8 +554,8 @@  main(int argc, char *argv[])
 			if (cuttimes != loend && !*loend) {
 				hi = lo;
 				if (hi < cuthitime) {
-					if (hi < absolute_min_time)
-						hi = absolute_min_time;
+					if (hi < absolute_min_time + 1)
+					  hi = absolute_min_time + 1;
 					cuthitime = hi;
 				}
 			} else if (cuttimes != loend && *loend == ','
@@ -497,8 +567,8 @@  main(int argc, char *argv[])
 					cutlotime = lo;
 				}
 				if (hi < cuthitime) {
-					if (hi < absolute_min_time)
-						hi = absolute_min_time;
+					if (hi < absolute_min_time + 1)
+					  hi = absolute_min_time + 1;
 					cuthitime = hi;
 				}
 			} else {
@@ -510,14 +580,17 @@  main(int argc, char *argv[])
 		}
 	}
 	gmtzinit();
-	INITIALIZE (now);
-	if (! (iflag | vflag | Vflag))
+	if (iflag | vflag | Vflag)
+	  now = 0;
+	else {
 	  now = time(NULL);
+	  now |= !now;
+	}
 	longest = 0;
 	for (i = optind; i < argc; i++) {
 	  size_t arglen = strlen(argv[i]);
 	  if (longest < arglen)
-	    longest = arglen < INT_MAX ? arglen : INT_MAX;
+	    longest = min(arglen, INT_MAX);
 	}
 
 	for (i = optind; i < argc; ++i) {
@@ -527,10 +600,12 @@  main(int argc, char *argv[])
 		struct tm tm, newtm;
 		bool tm_ok;
 		if (!tz) {
-		  perror(argv[i]);
+		  char const *e = strerror(errno);
+		  fprintf(stderr, _("%s: unknown timezone '%s': %s\n"),
+			  progname, argv[i], e);
 		  return EXIT_FAILURE;
 		}
-		if (! (iflag | vflag | Vflag)) {
+		if (now) {
 			show(tz, argv[i], now, false);
 			tzfree(tz);
 			continue;
@@ -539,12 +614,15 @@  main(int argc, char *argv[])
 		t = absolute_min_time;
 		if (! (iflag | Vflag)) {
 			show(tz, argv[i], t, true);
-			t += SECSPERDAY;
-			show(tz, argv[i], t, true);
+			if (my_localtime_rz(tz, &t, &tm) == NULL
+			    && t < cutlotime) {
+				time_t newt = cutlotime;
+				if (my_localtime_rz(tz, &newt, &newtm) != NULL)
+				  showextrema(tz, argv[i], t, NULL, newt);
+			}
 		}
-		if (t < cutlotime)
-			t = cutlotime;
-		INITIALIZE (ab);
+		if (t + 1 < cutlotime)
+		  t = cutlotime - 1;
 		tm_ok = my_localtime_rz(tz, &t, &tm) != NULL;
 		if (tm_ok) {
 		  ab = saveabbr(&abbrev, &abbrevsize, &tm);
@@ -552,19 +630,20 @@  main(int argc, char *argv[])
 		    showtrans("\nTZ=%f", &tm, t, ab, argv[i]);
 		    showtrans("-\t-\t%Q", &tm, t, ab, argv[i]);
 		  }
-		}
-		while (t < cuthitime) {
+		} else
+		  ab = NULL;
+		while (t < cuthitime - 1) {
 		  time_t newt = ((t < absolute_max_time - SECSPERDAY / 2
-				  && t + SECSPERDAY / 2 < cuthitime)
+				  && t + SECSPERDAY / 2 < cuthitime - 1)
 				 ? t + SECSPERDAY / 2
-				 : cuthitime);
+				 : cuthitime - 1);
 		  struct tm *newtmp = localtime_rz(tz, &newt, &newtm);
 		  bool newtm_ok = newtmp != NULL;
 		  if (tm_ok != newtm_ok
-		      || (tm_ok && (delta(&newtm, &tm) != newt - t
-				    || newtm.tm_isdst != tm.tm_isdst
-				    || strcmp(abbr(&newtm), ab) != 0))) {
-		    newt = hunt(tz, argv[i], t, newt);
+		      || (ab && (delta(&newtm, &tm) != newt - t
+				 || newtm.tm_isdst != tm.tm_isdst
+				 || strcmp(abbr(&newtm), ab) != 0))) {
+		    newt = hunt(tz, t, newt, false);
 		    newtmp = localtime_rz(tz, &newt, &newtm);
 		    newtm_ok = newtmp != NULL;
 		    if (iflag)
@@ -583,11 +662,15 @@  main(int argc, char *argv[])
 		  }
 		}
 		if (! (iflag | Vflag)) {
-			t = absolute_max_time;
-			t -= SECSPERDAY;
-			show(tz, argv[i], t, true);
-			t += SECSPERDAY;
-			show(tz, argv[i], t, true);
+			time_t newt = absolute_max_time;
+			t = cuthitime;
+			if (t < newt) {
+			  struct tm *tmp = my_localtime_rz(tz, &t, &tm);
+			  if (tmp != NULL
+			      && my_localtime_rz(tz, &newt, &newtm) == NULL)
+			    showextrema(tz, argv[i], t, tmp, newt);
+			}
+			show(tz, argv[i], absolute_max_time, true);
 		}
 		tzfree(tz);
 	}
@@ -640,36 +723,42 @@  yeartot(intmax_t y)
 	return t;
 }
 
+/* Search for a discontinuity in timezone TZ, in the
+   timestamps ranging from LOT through HIT.  LOT and HIT disagree
+   about some aspect of timezone.  If ONLY_OK, search only for
+   definedness changes, i.e., localtime succeeds on one side of the
+   transition but fails on the other side.  Return the timestamp just
+   before the transition from LOT's settings.  */
+
 static time_t
-hunt(timezone_t tz, char *name, time_t lot, time_t hit)
+hunt(timezone_t tz, time_t lot, time_t hit, bool only_ok)
 {
 	static char *		loab;
-	static size_t		loabsize;
-	char const *		ab;
-	time_t			t;
+	static ptrdiff_t	loabsize;
 	struct tm		lotm;
 	struct tm		tm;
+
+	/* Convert LOT into a broken-down time here, even though our
+	   caller already did that.  On platforms without TM_ZONE,
+	   tzname may have been altered since our caller broke down
+	   LOT, and tzname needs to be changed back.  */
 	bool lotm_ok = my_localtime_rz(tz, &lot, &lotm) != NULL;
 	bool tm_ok;
+	char const *ab = lotm_ok ? saveabbr(&loab, &loabsize, &lotm) : NULL;
 
-	if (lotm_ok)
-	  ab = saveabbr(&loab, &loabsize, &lotm);
 	for ( ; ; ) {
-		time_t diff = hit - lot;
-		if (diff < 2)
+		/* T = average of LOT and HIT, rounding down.
+		   Avoid overflow.  */
+		int rem_sum = lot % 2 + hit % 2;
+		time_t t = (rem_sum == 2) - (rem_sum < 0) + lot / 2 + hit / 2;
+		if (t == lot)
 			break;
-		t = lot;
-		t += diff / 2;
-		if (t <= lot)
-			++t;
-		else if (t >= hit)
-			--t;
 		tm_ok = my_localtime_rz(tz, &t, &tm) != NULL;
-		if (lotm_ok & tm_ok
-		    ? (delta(&tm, &lotm) == t - lot
-		       && tm.tm_isdst == lotm.tm_isdst
-		       && strcmp(abbr(&tm), ab) == 0)
-		    : lotm_ok == tm_ok) {
+		if (lotm_ok == tm_ok
+		    && (only_ok
+			|| (ab && tm.tm_isdst == lotm.tm_isdst
+			    && delta(&tm, &lotm) == t - lot
+			    && strcmp(abbr(&tm), ab) == 0))) {
 		  lot = t;
 		  if (tm_ok)
 		    lotm = tm;
@@ -685,11 +774,11 @@  hunt(timezone_t tz, char *name, time_t lot, time_t hit)
 static intmax_t
 delta_nonneg(struct tm *newp, struct tm *oldp)
 {
-	register intmax_t	result;
-	register int		tmy;
-
-	result = 0;
-	for (tmy = oldp->tm_year; tmy < newp->tm_year; ++tmy)
+	intmax_t oldy = oldp->tm_year;
+	int cycles = (newp->tm_year - oldy) / YEARSPERREPEAT;
+	intmax_t sec = SECSPERREPEAT, result = cycles * sec;
+	int tmy = oldp->tm_year + cycles * YEARSPERREPEAT;
+	for ( ; tmy < newp->tm_year; ++tmy)
 		result += DAYSPERNYEAR + isleap_sum(tmy, TM_YEAR_BASE);
 	result += newp->tm_yday - oldp->tm_yday;
 	result *= HOURSPERDAY;
@@ -730,7 +819,8 @@  adjusted_yday(struct tm const *a, struct tm const *b)
    my_gmtime_r and use its result instead of B.  Otherwise, B is the
    possibly nonnull result of an earlier call to my_gmtime_r.  */
 static long
-gmtoff(struct tm const *a, time_t *t, struct tm const *b)
+gmtoff(struct tm const *a, ATTRIBUTE_MAYBE_UNUSED time_t *t,
+       ATTRIBUTE_MAYBE_UNUSED struct tm const *b)
 {
 #ifdef TM_GMTOFF
   return a->TM_GMTOFF;
@@ -764,6 +854,7 @@  show(timezone_t tz, char *zone, time_t t, bool v)
 		gmtmp = my_gmtime_r(&t, &gmtm);
 		if (gmtmp == NULL) {
 			printf(tformat(), t);
+			printf(_(" (gmtime failed)"));
 		} else {
 			dumptime(gmtmp);
 			printf(" UT");
@@ -771,8 +862,11 @@  show(timezone_t tz, char *zone, time_t t, bool v)
 		printf(" = ");
 	}
 	tmp = my_localtime_rz(tz, &t, &tm);
-	dumptime(tmp);
-	if (tmp != NULL) {
+	if (tmp == NULL) {
+		printf(tformat(), t);
+		printf(_(" (localtime failed)"));
+	} else {
+		dumptime(tmp);
 		if (*abbr(tmp) != '\0')
 			printf(" %s", abbr(tmp));
 		if (v) {
@@ -787,13 +881,58 @@  show(timezone_t tz, char *zone, time_t t, bool v)
 		abbrok(abbr(tmp), zone);
 }
 
+/* Show timestamps just before and just after a transition between
+   defined and undefined (or vice versa) in either localtime or
+   gmtime.  These transitions are for timezone TZ with name ZONE, in
+   the range from LO (with broken-down time LOTMP if that is nonnull)
+   through HI.  LO and HI disagree on definedness.  */
+
+static void
+showextrema(timezone_t tz, char *zone, time_t lo, struct tm *lotmp, time_t hi)
+{
+  struct tm localtm[2], gmtm[2];
+  time_t t, boundary = hunt(tz, lo, hi, true);
+  bool old = false;
+  hi = (SECSPERDAY < hi - boundary
+	? boundary + SECSPERDAY
+	: hi + (hi < TIME_T_MAX));
+  if (SECSPERDAY < boundary - lo) {
+    lo = boundary - SECSPERDAY;
+    lotmp = my_localtime_rz(tz, &lo, &localtm[old]);
+  }
+  if (lotmp)
+    localtm[old] = *lotmp;
+  else
+    localtm[old].tm_sec = -1;
+  if (! my_gmtime_r(&lo, &gmtm[old]))
+    gmtm[old].tm_sec = -1;
+
+  /* Search sequentially for definedness transitions.  Although this
+     could be sped up by refining 'hunt' to search for either
+     localtime or gmtime definedness transitions, it hardly seems
+     worth the trouble.  */
+  for (t = lo + 1; t < hi; t++) {
+    bool new = !old;
+    if (! my_localtime_rz(tz, &t, &localtm[new]))
+      localtm[new].tm_sec = -1;
+    if (! my_gmtime_r(&t, &gmtm[new]))
+      gmtm[new].tm_sec = -1;
+    if (((localtm[old].tm_sec < 0) != (localtm[new].tm_sec < 0))
+	| ((gmtm[old].tm_sec < 0) != (gmtm[new].tm_sec < 0))) {
+      show(tz, zone, t - 1, true);
+      show(tz, zone, t, true);
+    }
+    old = new;
+  }
+}
+
 #if HAVE_SNPRINTF
 # define my_snprintf snprintf
 #else
 # include <stdarg.h>
 
 /* A substitute for snprintf that is good enough for zdump.  */
-static int ATTRIBUTE_FORMAT((printf, 3, 4))
+ATTRIBUTE_FORMAT((printf, 3, 4)) static int
 my_snprintf(char *s, size_t size, char const *format, ...)
 {
   int n;
@@ -831,7 +970,7 @@  my_snprintf(char *s, size_t size, char const *format, ...)
    fit, return the length that the string would have been if it had
    fit; do not overrun the output buffer.  */
 static int
-format_local_time(char *buf, size_t size, struct tm const *tm)
+format_local_time(char *buf, ptrdiff_t size, struct tm const *tm)
 {
   int ss = tm->tm_sec, mm = tm->tm_min, hh = tm->tm_hour;
   return (ss
@@ -854,7 +993,7 @@  format_local_time(char *buf, size_t size, struct tm const *tm)
    the length that the string would have been if it had fit; do not
    overrun the output buffer.  */
 static int
-format_utc_offset(char *buf, size_t size, struct tm const *tm, time_t t)
+format_utc_offset(char *buf, ptrdiff_t size, struct tm const *tm, time_t t)
 {
   long off = gmtoff(tm, &t, NULL);
   char sign = ((off < 0
@@ -883,11 +1022,11 @@  format_utc_offset(char *buf, size_t size, struct tm const *tm, time_t t)
    If the representation's length is less than SIZE, return the
    length; the representation is not null terminated.  Otherwise
    return SIZE, to indicate that BUF is too small.  */
-static size_t
-format_quoted_string(char *buf, size_t size, char const *p)
+static ptrdiff_t
+format_quoted_string(char *buf, ptrdiff_t size, char const *p)
 {
   char *b = buf;
-  size_t s = size;
+  ptrdiff_t s = size;
   if (!s)
     return size;
   *b++ = '"', s--;
@@ -925,11 +1064,11 @@  format_quoted_string(char *buf, size_t size, char const *p)
       and omit any trailing tabs.  */
 
 static bool
-istrftime(char *buf, size_t size, char const *time_fmt,
+istrftime(char *buf, ptrdiff_t size, char const *time_fmt,
 	  struct tm const *tm, time_t t, char const *ab, char const *zone_name)
 {
   char *b = buf;
-  size_t s = size;
+  ptrdiff_t s = size;
   char const *f = time_fmt, *p;
 
   for (p = f; ; p++)
@@ -938,9 +1077,9 @@  istrftime(char *buf, size_t size, char const *time_fmt,
     else if (!*p
 	     || (*p == '%'
 		 && (p[1] == 'f' || p[1] == 'L' || p[1] == 'Q'))) {
-      size_t formatted_len;
-      size_t f_prefix_len = p - f;
-      size_t f_prefix_copy_size = p - f + 2;
+      ptrdiff_t formatted_len;
+      ptrdiff_t f_prefix_len = p - f;
+      ptrdiff_t f_prefix_copy_size = sumsize(f_prefix_len, 2);
       char fbuf[100];
       bool oversized = sizeof fbuf <= f_prefix_copy_size;
       char *f_prefix_copy = oversized ? xmalloc(f_prefix_copy_size) : fbuf;
@@ -972,7 +1111,7 @@  istrftime(char *buf, size_t size, char const *time_fmt,
 	  b += offlen, s -= offlen;
 	  if (show_abbr) {
 	    char const *abp;
-	    size_t len;
+	    ptrdiff_t len;
 	    if (s <= 1)
 	      return false;
 	    *b++ = '\t', s--;
@@ -1011,7 +1150,7 @@  showtrans(char const *time_fmt, struct tm const *tm, time_t t, char const *ab,
     putchar('\n');
   } else {
     char stackbuf[1000];
-    size_t size = sizeof stackbuf;
+    ptrdiff_t size = sizeof stackbuf;
     char *buf = stackbuf;
     char *bufalloc = NULL;
     while (! istrftime(buf, size, time_fmt, tm, t, ab, zone_name)) {
@@ -1040,28 +1179,45 @@  abbr(struct tm const *tmp)
 
 /*
 ** The code below can fail on certain theoretical systems;
-** it works on all known real-world systems as of 2004-12-30.
+** it works on all known real-world systems as of 2022-01-25.
 */
 
 static const char *
 tformat(void)
 {
+#if HAVE__GENERIC
+	/* C11-style _Generic is more likely to return the correct
+	   format when distinct types have the same size.  */
+	char const *fmt =
+	  _Generic(+ (time_t) 0,
+		   int: "%d", long: "%ld", long long: "%lld",
+		   unsigned: "%u", unsigned long: "%lu",
+		   unsigned long long: "%llu",
+		   default: NULL);
+	if (fmt)
+	  return fmt;
+	fmt = _Generic((time_t) 0,
+		       intmax_t: "%"PRIdMAX, uintmax_t: "%"PRIuMAX,
+		       default: NULL);
+	if (fmt)
+	  return fmt;
+#endif
 	if (0 > (time_t) -1) {		/* signed */
-		if (sizeof (time_t) == sizeof (intmax_t))
+		if (sizeof(time_t) == sizeof(intmax_t))
 			return "%"PRIdMAX;
-		if (sizeof (time_t) > sizeof (long))
+		if (sizeof(time_t) > sizeof(long))
 			return "%lld";
-		if (sizeof (time_t) > sizeof (int))
+		if (sizeof(time_t) > sizeof(int))
 			return "%ld";
 		return "%d";
 	}
 #ifdef PRIuMAX
-	if (sizeof (time_t) == sizeof (uintmax_t))
+	if (sizeof(time_t) == sizeof(uintmax_t))
 		return "%"PRIuMAX;
 #endif
-	if (sizeof (time_t) > sizeof (unsigned long))
+	if (sizeof(time_t) > sizeof(unsigned long))
 		return "%llu";
-	if (sizeof (time_t) > sizeof (unsigned int))
+	if (sizeof(time_t) > sizeof(unsigned int))
 		return "%lu";
 	return "%u";
 }
@@ -1076,33 +1232,24 @@  dumptime(register const struct tm *timeptr)
 		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
 		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
 	};
-	register const char *	wn;
-	register const char *	mn;
 	register int		lead;
 	register int		trail;
+	int DIVISOR = 10;
 
-	if (timeptr == NULL) {
-		printf("NULL");
-		return;
-	}
 	/*
 	** The packaged localtime_rz and gmtime_r never put out-of-range
 	** values in tm_wday or tm_mon, but since this code might be compiled
 	** with other (perhaps experimental) versions, paranoia is in order.
 	*/
-	if (timeptr->tm_wday < 0 || timeptr->tm_wday >=
-		(int) (sizeof wday_name / sizeof wday_name[0]))
-			wn = "???";
-	else		wn = wday_name[timeptr->tm_wday];
-	if (timeptr->tm_mon < 0 || timeptr->tm_mon >=
-		(int) (sizeof mon_name / sizeof mon_name[0]))
-			mn = "???";
-	else		mn = mon_name[timeptr->tm_mon];
 	printf("%s %s%3d %.2d:%.2d:%.2d ",
-		wn, mn,
+		((0 <= timeptr->tm_wday
+		  && timeptr->tm_wday < sizeof wday_name / sizeof wday_name[0])
+		 ? wday_name[timeptr->tm_wday] : "???"),
+		((0 <= timeptr->tm_mon
+		  && timeptr->tm_mon < sizeof mon_name / sizeof mon_name[0])
+		 ? mon_name[timeptr->tm_mon] : "???"),
 		timeptr->tm_mday, timeptr->tm_hour,
 		timeptr->tm_min, timeptr->tm_sec);
-#define DIVISOR	10
 	trail = timeptr->tm_year % DIVISOR + TM_YEAR_BASE % DIVISOR;
 	lead = timeptr->tm_year / DIVISOR + TM_YEAR_BASE / DIVISOR +
 		trail / DIVISOR;
diff --git a/timezone/zic.c b/timezone/zic.c
index 2875b5544c..00f00e307a 100644
--- a/timezone/zic.c
+++ b/timezone/zic.c
@@ -5,29 +5,42 @@ 
 ** 2006-07-17 by Arthur David Olson.
 */
 
+/* Use the system 'time' function, instead of any private replacement.
+   This avoids creating an unnecessary dependency on localtime.c.  */
+#undef EPOCH_LOCAL
+#undef EPOCH_OFFSET
+#undef RESERVE_STD_EXT_IDS
+#undef time_tz
+
 #include "version.h"
 #include "private.h"
+#include "tzdir.h"
 #include "tzfile.h"
 
 #include <fcntl.h>
 #include <locale.h>
+#include <signal.h>
 #include <stdarg.h>
-#include <stddef.h>
 #include <stdio.h>
 
-#define	ZIC_VERSION_PRE_2013 '2'
-#define	ZIC_VERSION	'3'
-
 typedef int_fast64_t	zic_t;
-#define ZIC_MIN INT_FAST64_MIN
-#define ZIC_MAX INT_FAST64_MAX
-#define PRIdZIC PRIdFAST64
+static zic_t const
+  ZIC_MIN = INT_FAST64_MIN,
+  ZIC_MAX = INT_FAST64_MAX,
+  ZIC32_MIN = -1 - (zic_t) 0x7fffffff,
+  ZIC32_MAX = 0x7fffffff;
 #define SCNdZIC SCNdFAST64
 
 #ifndef ZIC_MAX_ABBR_LEN_WO_WARN
-#define ZIC_MAX_ABBR_LEN_WO_WARN	6
+# define ZIC_MAX_ABBR_LEN_WO_WARN 6
 #endif /* !defined ZIC_MAX_ABBR_LEN_WO_WARN */
 
+/* Minimum and maximum years, assuming signed 32-bit time_t.  */
+enum { YEAR_32BIT_MIN = 1901, YEAR_32BIT_MAX = 2038 };
+
+/* An upper bound on how much a format might grow due to concatenation.  */
+enum { FORMAT_LEN_GROWTH_BOUND = 5 };
+
 #ifdef HAVE_DIRECT_H
 # include <direct.h>
 # include <io.h>
@@ -35,38 +48,40 @@  typedef int_fast64_t	zic_t;
 # define mkdir(name, mode) _mkdir(name)
 #endif
 
+#ifndef HAVE_GETRANDOM
+# ifdef __has_include
+#  if __has_include(<sys/random.h>)
+#   include <sys/random.h>
+#  endif
+# elif 2 < __GLIBC__ + (25 <= __GLIBC_MINOR__)
+#  include <sys/random.h>
+# endif
+# define HAVE_GETRANDOM GRND_RANDOM
+#elif HAVE_GETRANDOM
+# include <sys/random.h>
+#endif
+
 #if HAVE_SYS_STAT_H
-#include <sys/stat.h>
+# include <sys/stat.h>
 #endif
 #ifdef S_IRUSR
-#define MKDIR_UMASK (S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH)
+# define MKDIR_UMASK (S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH)
 #else
-#define MKDIR_UMASK 0755
+# define MKDIR_UMASK 0755
 #endif
-/* Port to native MS-Windows and to ancient UNIX.  */
-#if !defined S_ISDIR && defined S_IFDIR && defined S_IFMT
-# define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
-#endif
-
-#if HAVE_SYS_WAIT_H
-#include <sys/wait.h>	/* for WIFEXITED and WEXITSTATUS */
-#endif /* HAVE_SYS_WAIT_H */
-
-#ifndef WIFEXITED
-#define WIFEXITED(status)	(((status) & 0xff) == 0)
-#endif /* !defined WIFEXITED */
-#ifndef WEXITSTATUS
-#define WEXITSTATUS(status)	(((status) >> 8) & 0xff)
-#endif /* !defined WEXITSTATUS */
 
-/* The maximum ptrdiff_t value, for pre-C99 platforms.  */
-#ifndef PTRDIFF_MAX
-static ptrdiff_t const PTRDIFF_MAX = MAXVAL(ptrdiff_t, TYPE_BIT(ptrdiff_t));
+/* The minimum alignment of a type, for pre-C23 platforms.
+   The __SUNPRO_C test is because Oracle Developer Studio 12.6 lacks
+   <stdalign.h> even though __STDC_VERSION__ == 201112.  */
+#if __STDC_VERSION__ < 201112 || defined __SUNPRO_C
+# define alignof(type) offsetof(struct { char a; type b; }, b)
+#elif __STDC_VERSION__ < 202311
+# include <stdalign.h>
 #endif
 
-/* The minimum alignment of a type, for pre-C11 platforms.  */
-#if __STDC_VERSION__ < 201112
-# define _Alignof(type) offsetof(struct { char a; type b; }, b)
+/* The maximum length of a text line, including the trailing newline.  */
+#ifndef _POSIX2_LINE_MAX
+# define _POSIX2_LINE_MAX 2048
 #endif
 
 /* The type for line numbers.  Use PRIdMAX to format them; formerly
@@ -75,14 +90,12 @@  static ptrdiff_t const PTRDIFF_MAX = MAXVAL(ptrdiff_t, TYPE_BIT(ptrdiff_t));
 typedef intmax_t lineno;
 
 struct rule {
-	const char *	r_filename;
+	int		r_filenum;
 	lineno		r_linenum;
 	const char *	r_name;
 
 	zic_t		r_loyear;	/* for example, 1986 */
 	zic_t		r_hiyear;	/* for example, 1986 */
-	const char *	r_yrtype;
-	bool		r_lowasnum;
 	bool		r_hiwasnum;
 
 	int		r_month;	/* 0..11 */
@@ -103,15 +116,16 @@  struct rule {
 };
 
 /*
-**	r_dycode		r_dayofmonth	r_wday
+** r_dycode	r_dayofmonth	r_wday
 */
-
-#define DC_DOM		0	/* 1..31 */	/* unused */
-#define DC_DOWGEQ	1	/* 1..31 */	/* 0..6 (Sun..Sat) */
-#define DC_DOWLEQ	2	/* 1..31 */	/* 0..6 (Sun..Sat) */
+enum {
+  DC_DOM,	/* 1..31 */	/* unused */
+  DC_DOWGEQ,	/* 1..31 */	/* 0..6 (Sun..Sat) */
+  DC_DOWLEQ	/* 1..31 */	/* 0..6 (Sun..Sat) */
+};
 
 struct zone {
-	const char *	z_filename;
+	int		z_filenum;
 	lineno		z_linenum;
 
 	const char *	z_name;
@@ -133,22 +147,28 @@  struct zone {
 #if !HAVE_POSIX_DECLS
 extern int	getopt(int argc, char * const argv[],
 			const char * options);
-extern int	link(const char * fromname, const char * toname);
+extern int	link(const char * target, const char * linkname);
 extern char *	optarg;
 extern int	optind;
 #endif
 
-#if ! HAVE_LINK
-# define link(from, to) (errno = ENOTSUP, -1)
-#endif
 #if ! HAVE_SYMLINK
-# define readlink(file, buf, size) (errno = ENOTSUP, -1)
-# define symlink(from, to) (errno = ENOTSUP, -1)
-# define S_ISLNK(m) 0
+static ssize_t
+readlink(char const *restrict file, char *restrict buf, size_t size)
+{
+  errno = ENOTSUP;
+  return -1;
+}
+static int
+symlink(char const *target, char const *linkname)
+{
+  errno = ENOTSUP;
+  return -1;
+}
 #endif
 #ifndef AT_SYMLINK_FOLLOW
-# define linkat(fromdir, from, todir, to, flag) \
-    (itssymlink(from) ? (errno = ENOTSUP, -1) : link(from, to))
+#  define linkat(targetdir, target, linknamedir, linkname, flag) \
+     (errno = ENOTSUP, -1)
 #endif
 
 static void	addtt(zic_t starttime, int type);
@@ -157,19 +177,18 @@  static void	leapadd(zic_t, int, int);
 static void	adjleap(void);
 static void	associate(void);
 static void	dolink(const char *, const char *, bool);
-static char **	getfields(char * buf);
+static int	getfields(char *, char **, int);
 static zic_t	gethms(const char * string, const char * errstring);
 static zic_t	getsave(char *, bool *);
 static void	inexpires(char **, int);
-static void	infile(const char * filename);
+static void	infile(int, char const *);
 static void	inleap(char ** fields, int nfields);
 static void	inlink(char ** fields, int nfields);
 static void	inrule(char ** fields, int nfields);
 static bool	inzcont(char ** fields, int nfields);
 static bool	inzone(char ** fields, int nfields);
 static bool	inzsub(char **, int, bool);
-static bool	itsdir(char const *);
-static bool	itssymlink(char const *);
+static int	itssymlink(char const *, int *);
 static bool	is_alpha(char a);
 static char	lowerit(char);
 static void	mkdirs(char const *, bool);
@@ -177,29 +196,19 @@  static void	newabbr(const char * abbr);
 static zic_t	oadd(zic_t t1, zic_t t2);
 static void	outzone(const struct zone * zp, ptrdiff_t ntzones);
 static zic_t	rpytime(const struct rule * rp, zic_t wantedy);
-static void	rulesub(struct rule * rp,
+static bool	rulesub(struct rule * rp,
 			const char * loyearp, const char * hiyearp,
 			const char * typep, const char * monthp,
 			const char * dayp, const char * timep);
 static zic_t	tadd(zic_t t1, zic_t t2);
-static bool	yearistype(zic_t year, const char * type);
 
 /* Bound on length of what %z can expand to.  */
 enum { PERCENT_Z_LEN_BOUND = sizeof "+995959" - 1 };
 
-/* If true, work around a bug in Qt 5.6.1 and earlier, which mishandles
-   TZif files whose POSIX-TZ-style strings contain '<'; see
-   QTBUG-53071 <https://bugreports.qt.io/browse/QTBUG-53071>.  This
-   workaround will no longer be needed when Qt 5.6.1 and earlier are
-   obsolete, say in the year 2021.  */
-#ifndef WORK_AROUND_QTBUG_53071
-enum { WORK_AROUND_QTBUG_53071 = true };
-#endif
-
 static int		charcnt;
 static bool		errors;
 static bool		warnings;
-static const char *	filename;
+static int		filenum;
 static int		leapcnt;
 static bool		leapseen;
 static zic_t		leapminyear;
@@ -210,97 +219,121 @@  static int		max_format_len;
 static zic_t		max_year;
 static zic_t		min_year;
 static bool		noise;
-static const char *	rfilename;
+static int		rfilenum;
 static lineno		rlinenum;
 static const char *	progname;
+static char const *	leapsec;
+static char *const *	main_argv;
 static ptrdiff_t	timecnt;
 static ptrdiff_t	timecnt_alloc;
 static int		typecnt;
+static int		unspecifiedtype;
 
 /*
 ** Line codes.
 */
 
-#define LC_RULE		0
-#define LC_ZONE		1
-#define LC_LINK		2
-#define LC_LEAP		3
-#define LC_EXPIRES	4
+enum {
+  LC_RULE,
+  LC_ZONE,
+  LC_LINK,
+  LC_LEAP,
+  LC_EXPIRES
+};
 
 /*
 ** Which fields are which on a Zone line.
 */
 
-#define ZF_NAME		1
-#define ZF_STDOFF	2
-#define ZF_RULE		3
-#define ZF_FORMAT	4
-#define ZF_TILYEAR	5
-#define ZF_TILMONTH	6
-#define ZF_TILDAY	7
-#define ZF_TILTIME	8
-#define ZONE_MINFIELDS	5
-#define ZONE_MAXFIELDS	9
+enum {
+  ZF_NAME = 1,
+  ZF_STDOFF,
+  ZF_RULE,
+  ZF_FORMAT,
+  ZF_TILYEAR,
+  ZF_TILMONTH,
+  ZF_TILDAY,
+  ZF_TILTIME,
+  ZONE_MAXFIELDS,
+  ZONE_MINFIELDS = ZF_TILYEAR
+};
 
 /*
 ** Which fields are which on a Zone continuation line.
 */
 
-#define ZFC_STDOFF	0
-#define ZFC_RULE	1
-#define ZFC_FORMAT	2
-#define ZFC_TILYEAR	3
-#define ZFC_TILMONTH	4
-#define ZFC_TILDAY	5
-#define ZFC_TILTIME	6
-#define ZONEC_MINFIELDS	3
-#define ZONEC_MAXFIELDS	7
+enum {
+  ZFC_STDOFF,
+  ZFC_RULE,
+  ZFC_FORMAT,
+  ZFC_TILYEAR,
+  ZFC_TILMONTH,
+  ZFC_TILDAY,
+  ZFC_TILTIME,
+  ZONEC_MAXFIELDS,
+  ZONEC_MINFIELDS = ZFC_TILYEAR
+};
 
 /*
 ** Which files are which on a Rule line.
 */
 
-#define RF_NAME		1
-#define RF_LOYEAR	2
-#define RF_HIYEAR	3
-#define RF_COMMAND	4
-#define RF_MONTH	5
-#define RF_DAY		6
-#define RF_TOD		7
-#define RF_SAVE		8
-#define RF_ABBRVAR	9
-#define RULE_FIELDS	10
+enum {
+  RF_NAME = 1,
+  RF_LOYEAR,
+  RF_HIYEAR,
+  RF_COMMAND,
+  RF_MONTH,
+  RF_DAY,
+  RF_TOD,
+  RF_SAVE,
+  RF_ABBRVAR,
+  RULE_FIELDS
+};
 
 /*
 ** Which fields are which on a Link line.
 */
 
-#define LF_FROM		1
-#define LF_TO		2
-#define LINK_FIELDS	3
+enum {
+  LF_TARGET = 1,
+  LF_LINKNAME,
+  LINK_FIELDS
+};
 
 /*
 ** Which fields are which on a Leap line.
 */
 
-#define LP_YEAR		1
-#define LP_MONTH	2
-#define LP_DAY		3
-#define LP_TIME		4
-#define LP_CORR		5
-#define LP_ROLL		6
-#define LEAP_FIELDS	7
+enum {
+  LP_YEAR = 1,
+  LP_MONTH,
+  LP_DAY,
+  LP_TIME,
+  LP_CORR,
+  LP_ROLL,
+  LEAP_FIELDS,
+
+  /* Expires lines are like Leap lines, except without CORR and ROLL fields.  */
+  EXPIRES_FIELDS = LP_TIME + 1
+};
 
-/* Expires lines are like Leap lines, except without CORR and ROLL fields.  */
-#define EXPIRES_FIELDS	5
+/* The maximum number of fields on any of the above lines.
+   (The "+"s pacify gcc -Wenum-compare.)  */
+enum {
+  MAX_FIELDS = max(max(+RULE_FIELDS, +LINK_FIELDS),
+		   max(+LEAP_FIELDS, +EXPIRES_FIELDS))
+};
 
 /*
 ** Year synonyms.
 */
 
-#define YR_MINIMUM	0
-#define YR_MAXIMUM	1
-#define YR_ONLY		2
+enum {
+  YR_MINIMUM, /* "minimum" is for backward compatibility only */
+  YR_MAXIMUM,
+  YR_ONLY
+};
 
 static struct rule *	rules;
 static ptrdiff_t	nrules;	/* number of rules */
@@ -311,10 +344,10 @@  static ptrdiff_t	nzones;	/* number of zones */
 static ptrdiff_t	nzones_alloc;
 
 struct link {
-	const char *	l_filename;
+	int		l_filenum;
 	lineno		l_linenum;
-	const char *	l_from;
-	const char *	l_to;
+	const char *	l_target;
+	const char *	l_linkname;
 };
 
 static struct link *	links;
@@ -381,12 +414,10 @@  static struct lookup const	lasts[] = {
 
 static struct lookup const	begin_years[] = {
 	{ "minimum",	YR_MINIMUM },
-	{ "maximum",	YR_MAXIMUM },
 	{ NULL,		0 }
 };
 
 static struct lookup const	end_years[] = {
-	{ "minimum",	YR_MINIMUM },
 	{ "maximum",	YR_MAXIMUM },
 	{ "only",	YR_ONLY },
 	{ NULL,		0 }
@@ -426,29 +457,53 @@  static char		roll[TZ_MAX_LEAPS];
 ** Memory allocation.
 */
 
-static _Noreturn void
+ATTRIBUTE_NORETURN static void
 memory_exhausted(const char *msg)
 {
 	fprintf(stderr, _("%s: Memory exhausted: %s\n"), progname, msg);
 	exit(EXIT_FAILURE);
 }
 
-static ATTRIBUTE_PURE size_t
-size_product(size_t nitems, size_t itemsize)
+ATTRIBUTE_NORETURN static void
+size_overflow(void)
 {
-	if (SIZE_MAX / itemsize < nitems)
-		memory_exhausted(_("size overflow"));
-	return nitems * itemsize;
+  memory_exhausted(_("size overflow"));
 }
 
-static ATTRIBUTE_PURE size_t
-align_to(size_t size, size_t alignment)
+ATTRIBUTE_REPRODUCIBLE static ptrdiff_t
+size_sum(size_t a, size_t b)
 {
-  size_t aligned_size = size + alignment - 1;
-  aligned_size -= aligned_size % alignment;
-  if (aligned_size < size)
-    memory_exhausted(_("alignment overflow"));
-  return aligned_size;
+#ifdef ckd_add
+  ptrdiff_t sum;
+  if (!ckd_add(&sum, a, b) && sum <= INDEX_MAX)
+    return sum;
+#else
+  if (a <= INDEX_MAX && b <= INDEX_MAX - a)
+    return a + b;
+#endif
+  size_overflow();
+}
+
+ATTRIBUTE_REPRODUCIBLE static ptrdiff_t
+size_product(ptrdiff_t nitems, ptrdiff_t itemsize)
+{
+#ifdef ckd_mul
+  ptrdiff_t product;
+  if (!ckd_mul(&product, nitems, itemsize) && product <= INDEX_MAX)
+    return product;
+#else
+  ptrdiff_t nitems_max = INDEX_MAX / itemsize;
+  if (nitems <= nitems_max)
+    return nitems * itemsize;
+#endif
+  size_overflow();
+}
+
+ATTRIBUTE_REPRODUCIBLE static ptrdiff_t
+align_to(ptrdiff_t size, ptrdiff_t alignment)
+{
+  ptrdiff_t lo_bits = alignment - 1, sum = size_sum(size, lo_bits);
+  return sum & ~lo_bits;
 }
 
 #if !HAVE_STRDUP
@@ -464,11 +519,11 @@  static void *
 memcheck(void *ptr)
 {
 	if (ptr == NULL)
-		memory_exhausted(strerror(errno));
+	  memory_exhausted(strerror(HAVE_MALLOC_ERRNO ? errno : ENOMEM));
 	return ptr;
 }
 
-static void * ATTRIBUTE_MALLOC
+ATTRIBUTE_MALLOC static void *
 emalloc(size_t size)
 {
   return memcheck(malloc(size));
@@ -480,47 +535,76 @@  erealloc(void *ptr, size_t size)
   return memcheck(realloc(ptr, size));
 }
 
-static char * ATTRIBUTE_MALLOC
-ecpyalloc (char const *str)
+ATTRIBUTE_MALLOC static char *
+estrdup(char const *str)
 {
   return memcheck(strdup(str));
 }
 
+static ptrdiff_t
+grow_nitems_alloc(ptrdiff_t *nitems_alloc, ptrdiff_t itemsize)
+{
+  ptrdiff_t addend = (*nitems_alloc >> 1) + 1;
+#if defined ckd_add && defined ckd_mul
+  ptrdiff_t product;
+  if (!ckd_add(nitems_alloc, *nitems_alloc, addend)
+      && !ckd_mul(&product, *nitems_alloc, itemsize) && product <= INDEX_MAX)
+    return product;
+#else
+  if (*nitems_alloc <= ((INDEX_MAX - 1) / 3 * 2) / itemsize) {
+    *nitems_alloc += addend;
+    return *nitems_alloc * itemsize;
+  }
+#endif
+  memory_exhausted(_("integer overflow"));
+}
+
 static void *
-growalloc(void *ptr, size_t itemsize, ptrdiff_t nitems, ptrdiff_t *nitems_alloc)
+growalloc(void *ptr, ptrdiff_t itemsize, ptrdiff_t nitems,
+	  ptrdiff_t *nitems_alloc)
 {
-	if (nitems < *nitems_alloc)
-		return ptr;
-	else {
-		ptrdiff_t nitems_max = PTRDIFF_MAX - WORK_AROUND_QTBUG_53071;
-		ptrdiff_t amax = nitems_max < SIZE_MAX ? nitems_max : SIZE_MAX;
-		if ((amax - 1) / 3 * 2 < *nitems_alloc)
-			memory_exhausted(_("integer overflow"));
-		*nitems_alloc += (*nitems_alloc >> 1) + 1;
-		return erealloc(ptr, size_product(*nitems_alloc, itemsize));
-	}
+  return (nitems < *nitems_alloc
+	  ? ptr
+	  : erealloc(ptr, grow_nitems_alloc(nitems_alloc, itemsize)));
 }
 
 /*
 ** Error handling.
 */
 
+/* In most of the code, an input file name is represented by its index
+   into the main argument vector, except that LEAPSEC_FILENUM stands
+   for leapsec and COMMAND_LINE_FILENUM stands for the command line.  */
+enum { LEAPSEC_FILENUM = -2, COMMAND_LINE_FILENUM = -1 };
+
+/* Return the name of the Ith input file, for diagnostics.  */
+static char const *
+filename(int i)
+{
+  if (i == COMMAND_LINE_FILENUM)
+    return _("command line");
+  else {
+    char const *fname = i == LEAPSEC_FILENUM ? leapsec : main_argv[i];
+    return strcmp(fname, "-") == 0 ? _("standard input") : fname;
+  }
+}
+
 static void
-eats(char const *name, lineno num, char const *rname, lineno rnum)
+eats(int fnum, lineno num, int rfnum, lineno rnum)
 {
-	filename = name;
+	filenum = fnum;
 	linenum = num;
-	rfilename = rname;
+	rfilenum = rfnum;
 	rlinenum = rnum;
 }
 
 static void
-eat(char const *name, lineno num)
+eat(int fnum, lineno num)
 {
-	eats(name, num, NULL, -1);
+	eats(fnum, num, 0, -1);
 }
 
-static void ATTRIBUTE_FORMAT((printf, 1, 0))
+ATTRIBUTE_FORMAT((printf, 1, 0)) static void
 verror(const char *const string, va_list args)
 {
 	/*
@@ -528,16 +612,17 @@  verror(const char *const string, va_list args)
 	**	zic ... 2>&1 | error -t "*" -v
 	** on BSD systems.
 	*/
-	if (filename)
-	  fprintf(stderr, _("\"%s\", line %"PRIdMAX": "), filename, linenum);
+	if (filenum)
+	  fprintf(stderr, _("\"%s\", line %"PRIdMAX": "),
+		  filename(filenum), linenum);
 	vfprintf(stderr, string, args);
-	if (rfilename != NULL)
+	if (rfilenum)
 		fprintf(stderr, _(" (rule from \"%s\", line %"PRIdMAX")"),
-			rfilename, rlinenum);
+			filename(rfilenum), rlinenum);
 	fprintf(stderr, "\n");
 }
 
-static void ATTRIBUTE_FORMAT((printf, 1, 2))
+ATTRIBUTE_FORMAT((printf, 1, 2)) static void
 error(const char *const string, ...)
 {
 	va_list args;
@@ -547,7 +632,7 @@  error(const char *const string, ...)
 	errors = true;
 }
 
-static void ATTRIBUTE_FORMAT((printf, 1, 2))
+ATTRIBUTE_FORMAT((printf, 1, 2)) static void
 warning(const char *const string, ...)
 {
 	va_list args;
@@ -558,8 +643,11 @@  warning(const char *const string, ...)
 	warnings = true;
 }
 
+/* Close STREAM.  If it had an I/O error, report it against DIR/NAME,
+   remove TEMPNAME if nonnull, and then exit.  */
 static void
-close_file(FILE *stream, char const *dir, char const *name)
+close_file(FILE *stream, char const *dir, char const *name,
+	   char const *tempname)
 {
   char const *e = (ferror(stream) ? _("I/O error")
 		   : fclose(stream) != 0 ? strerror(errno) : NULL);
@@ -568,23 +656,26 @@  close_file(FILE *stream, char const *dir, char const *name)
 	    dir ? dir : "", dir ? "/" : "",
 	    name ? name : "", name ? ": " : "",
 	    e);
+    if (tempname)
+      remove(tempname);
     exit(EXIT_FAILURE);
   }
 }
 
-static _Noreturn void
+ATTRIBUTE_NORETURN static void
 usage(FILE *stream, int status)
 {
   fprintf(stream,
 	  _("%s: usage is %s [ --version ] [ --help ] [ -v ] \\\n"
 	    "\t[ -b {slim|fat} ] [ -d directory ] [ -l localtime ]"
 	    " [ -L leapseconds ] \\\n"
-	    "\t[ -p posixrules ] [ -r '[@lo][/@hi]' ] [ -t localtime-link ] \\\n"
+	    "\t[ -p posixrules ] [ -r '[@lo][/@hi]' ] [ -R '@hi' ] \\\n"
+	    "\t[ -t localtime-link ] \\\n"
 	    "\t[ filename ... ]\n\n"
 	    "Report bugs to %s.\n"),
 	  progname, progname, REPORT_BUGS_TO);
   if (status == EXIT_SUCCESS)
-    close_file(stream, NULL, NULL);
+    close_file(stream, NULL, NULL, NULL);
   exit(status);
 }
 
@@ -592,7 +683,7 @@  usage(FILE *stream, int status)
    ancestors.  After this is done, all files are accessed with names
    relative to DIR.  */
 static void
-change_directory (char const *dir)
+change_directory(char const *dir)
 {
   if (chdir(dir) != 0) {
     int chdir_errno = errno;
@@ -608,7 +699,194 @@  change_directory (char const *dir)
   }
 }
 
-#define TIME_T_BITS_IN_FILE 64
+/* Compare the two links A and B, for a stable sort by link name.  */
+static int
+qsort_linkcmp(void const *a, void const *b)
+{
+  struct link const *l = a;
+  struct link const *m = b;
+  int cmp = strcmp(l->l_linkname, m->l_linkname);
+  if (cmp)
+    return cmp;
+
+  /* The link names are the same.  Make the sort stable by comparing
+     file numbers (where subtraction cannot overflow) and possibly
+     line numbers (where it can).  */
+  cmp = l->l_filenum - m->l_filenum;
+  if (cmp)
+    return cmp;
+  return (l->l_linenum > m->l_linenum) - (l->l_linenum < m->l_linenum);
+}
+
+/* Compare the string KEY to the link B, for bsearch.  */
+static int
+bsearch_linkcmp(void const *key, void const *b)
+{
+  struct link const *m = b;
+  return strcmp(key, m->l_linkname);
+}
+
+/* Make the links specified by the Link lines.  */
+static void
+make_links(void)
+{
+  ptrdiff_t i, j, nalinks, pass_size;
+  if (1 < nlinks)
+    qsort(links, nlinks, sizeof *links, qsort_linkcmp);
+
+  /* Ignore each link superseded by a later link with the same name.  */
+  j = 0;
+  for (i = 0; i < nlinks; i++) {
+    while (i + 1 < nlinks
+	   && strcmp(links[i].l_linkname, links[i + 1].l_linkname) == 0)
+      i++;
+    links[j++] = links[i];
+  }
+  nlinks = pass_size = j;
+
+  /* Walk through the link array making links.  However,
+     if a link's target has not been made yet, append a copy to the
+     end of the array.  The end of the array will gradually fill
+     up with a small sorted subsequence of not-yet-made links.
+     nalinks counts all the links in the array, including copies.
+     When we reach the copied subsequence, it may still contain
+     a link to a not-yet-made link, so the process repeats.
+     At any given point in time, the link array consists of the
+     following subregions, where 0 <= i <= j <= nalinks and
+     0 <= nlinks <= nalinks:
+
+       0 .. (i - 1):
+	 links that either have been made, or have been copied to a
+	 later point point in the array (this later point can be in
+	 any of the three subregions)
+       i .. (j - 1):
+	 not-yet-made links for this pass
+       j .. (nalinks - 1):
+	 not-yet-made links that this pass has skipped because
+	 they were links to not-yet-made links
+
+     The first subregion might not be sorted if nlinks < i;
+     the other two subregions are sorted.  This algorithm does
+     not alter entries 0 .. (nlinks - 1), which remain sorted.
+
+     If there are L links, this algorithm is O(C*L*log(L)) where
+     C is the length of the longest link chain.  Usually C is
+     short (e.g., 3) though its worst-case value is L.  */
+
+  j = nalinks = nlinks;
+
+  for (i = 0; i < nalinks; i++) {
+    struct link *l;
+
+    eat(links[i].l_filenum, links[i].l_linenum);
+
+    /* If this pass examined all its links, start the next pass.  */
+    if (i == j) {
+      if (nalinks - i == pass_size) {
+	error(_("\"Link %s %s\" is part of a link cycle"),
+	      links[i].l_target, links[i].l_linkname);
+	break;
+      }
+      j = nalinks;
+      pass_size = nalinks - i;
+    }
+
+    /* Diagnose self links, which the cycle detection algorithm would not
+       otherwise catch.  */
+    if (strcmp(links[i].l_target, links[i].l_linkname) == 0) {
+      error(_("link %s targets itself"), links[i].l_target);
+      continue;
+    }
+
+    /* Make this link unless its target has not been made yet.  */
+    l = bsearch(links[i].l_target, &links[i + 1], j - (i + 1),
+		sizeof *links, bsearch_linkcmp);
+    if (!l)
+      l = bsearch(links[i].l_target, &links[j], nalinks - j,
+		  sizeof *links, bsearch_linkcmp);
+    if (!l)
+      dolink(links[i].l_target, links[i].l_linkname, false);
+    else {
+      /* The link target has not been made yet; copy the link to the end.  */
+      links = growalloc(links, sizeof *links, nalinks, &nlinks_alloc);
+      links[nalinks++] = links[i];
+    }
+
+    if (noise && i < nlinks) {
+      if (l)
+	warning(_("link %s targeting link %s mishandled by pre-2023 zic"),
+		links[i].l_linkname, links[i].l_target);
+      else if (bsearch(links[i].l_target, links, nlinks, sizeof *links,
+		       bsearch_linkcmp))
+	warning(_("link %s targeting link %s"),
+		links[i].l_linkname, links[i].l_target);
+    }
+  }
+}
+
+/* Simple signal handling: just set a flag that is checked
+   periodically outside critical sections.  To set up the handler,
+   prefer sigaction if available to close a signal race.  */
+
+static sig_atomic_t got_signal;
+
+static void
+signal_handler(int sig)
+{
+#ifndef SA_SIGINFO
+  signal(sig, signal_handler);
+#endif
+  got_signal = sig;
+}
+
+/* Arrange for SIGINT etc. to be caught by the handler.  */
+static void
+catch_signals(void)
+{
+  static int const signals[] = {
+#ifdef SIGHUP
+    SIGHUP,
+#endif
+    SIGINT,
+#ifdef SIGPIPE
+    SIGPIPE,
+#endif
+    SIGTERM
+  };
+  int i;
+  for (i = 0; i < sizeof signals / sizeof signals[0]; i++) {
+#ifdef SA_SIGINFO
+    struct sigaction act0, act;
+    act.sa_handler = signal_handler;
+    sigemptyset(&act.sa_mask);
+    act.sa_flags = 0;
+    if (sigaction(signals[i], &act, &act0) == 0
+	&& ! (act0.sa_flags & SA_SIGINFO) && act0.sa_handler == SIG_IGN) {
+      sigaction(signals[i], &act0, NULL);
+      got_signal = 0;
+    }
+#else
+    if (signal(signals[i], signal_handler) == SIG_IGN) {
+      signal(signals[i], SIG_IGN);
+      got_signal = 0;
+    }
+#endif
+  }
+}
+
+/* If a signal has arrived, terminate zic with appropriate status.  */
+static void
+check_for_signal(void)
+{
+  int sig = got_signal;
+  if (sig) {
+    signal(sig, SIG_DFL);
+    raise(sig);
+    abort(); /* A bug in 'raise'.  */
+  }
+}
+
+enum { TIME_T_BITS_IN_FILE = 64 };
 
 /* The minimum and maximum values representable in a TZif file.  */
 static zic_t const min_time = MINVAL(zic_t, TIME_T_BITS_IN_FILE);
@@ -619,12 +897,13 @@  static zic_t const max_time = MAXVAL(zic_t, TIME_T_BITS_IN_FILE);
 static zic_t lo_time = MINVAL(zic_t, TIME_T_BITS_IN_FILE);
 static zic_t hi_time = MAXVAL(zic_t, TIME_T_BITS_IN_FILE);
 
+/* The time specified by the -R option, defaulting to MIN_TIME;
+   or lo_time, whichever is greater.  */
+static zic_t redundant_time = MINVAL(zic_t, TIME_T_BITS_IN_FILE);
+
 /* The time specified by an Expires line, or negative if no such line.  */
 static zic_t leapexpires = -1;
 
-/* The time specified by an #expires comment, or negative if no such line.  */
-static zic_t comment_leapexpires = -1;
-
 /* Set the time range of the output to TIMERANGE.
    Return true if successful.  */
 static bool
@@ -634,35 +913,49 @@  timerange_option(char *timerange)
   char *lo_end = timerange, *hi_end;
   if (*timerange == '@') {
     errno = 0;
-    lo = strtoimax (timerange + 1, &lo_end, 10);
+    lo = strtoimax(timerange + 1, &lo_end, 10);
     if (lo_end == timerange + 1 || (lo == INTMAX_MAX && errno == ERANGE))
       return false;
   }
   hi_end = lo_end;
   if (lo_end[0] == '/' && lo_end[1] == '@') {
     errno = 0;
-    hi = strtoimax (lo_end + 2, &hi_end, 10);
+    hi = strtoimax(lo_end + 2, &hi_end, 10);
     if (hi_end == lo_end + 2 || hi == INTMAX_MIN)
       return false;
     hi -= ! (hi == INTMAX_MAX && errno == ERANGE);
   }
   if (*hi_end || hi < lo || max_time < lo || hi < min_time)
     return false;
-  lo_time = lo < min_time ? min_time : lo;
-  hi_time = max_time < hi ? max_time : hi;
+  lo_time = max(lo, min_time);
+  hi_time = min(hi, max_time);
   return true;
 }
 
+/* Generate redundant time stamps up to OPT.  Return true if successful.  */
+static bool
+redundant_time_option(char *opt)
+{
+  if (*opt == '@') {
+    intmax_t redundant;
+    char *opt_end;
+    redundant = strtoimax(opt + 1, &opt_end, 10);
+    if (opt_end != opt + 1 && !*opt_end) {
+      redundant_time = max(redundant_time, redundant);
+      return true;
+    }
+  }
+  return false;
+}
+
 static const char *	psxrules;
 static const char *	lcltime;
 static const char *	directory;
-static const char *	leapsec;
 static const char *	tzdefault;
-static const char *	yitcommand;
 
 /* -1 if the TZif output file should be slim, 0 if default, 1 if the
-   output should be fat for backward compatibility.  Currently the
-   default is fat, although this may change.  */
+   output should be fat for backward compatibility.  ZIC_BLOAT_DEFAULT
+   determines the default.  */
 static int bloat;
 
 static bool
@@ -672,7 +965,7 @@  want_bloat(void)
 }
 
 #ifndef ZIC_BLOAT_DEFAULT
-# define ZIC_BLOAT_DEFAULT "fat"
+# define ZIC_BLOAT_DEFAULT "slim"
 #endif
 
 int
@@ -687,12 +980,13 @@  main(int argc, char **argv)
 #endif
 #if HAVE_GETTEXT
 	setlocale(LC_ALL, "");
-#ifdef TZ_DOMAINDIR
+# ifdef TZ_DOMAINDIR
 	bindtextdomain(TZ_DOMAIN, TZ_DOMAINDIR);
-#endif /* defined TEXTDOMAINDIR */
+# endif /* defined TEXTDOMAINDIR */
 	textdomain(TZ_DOMAIN);
 #endif /* HAVE_GETTEXT */
-	progname = argv[0];
+	main_argv = argv;
+	progname = argv[0] ? argv[0] : "zic";
 	if (TYPE_BIT(zic_t) < 64) {
 		fprintf(stderr, "%s: %s\n", progname,
 			_("wild compilation-time specification of zic_t"));
@@ -701,12 +995,13 @@  main(int argc, char **argv)
 	for (k = 1; k < argc; k++)
 		if (strcmp(argv[k], "--version") == 0) {
 			printf("zic %s%s\n", PKGVERSION, TZVERSION);
-			close_file(stdout, NULL, NULL);
+			close_file(stdout, NULL, NULL, NULL);
 			return EXIT_SUCCESS;
 		} else if (strcmp(argv[k], "--help") == 0) {
 			usage(stdout, EXIT_SUCCESS);
 		}
-	while ((c = getopt(argc, argv, "b:d:l:L:p:r:st:vy:")) != EOF && c != -1)
+	while ((c = getopt(argc, argv, "b:d:l:L:p:r:R:st:vy:")) != EOF
+	       && c != -1)
 		switch (c) {
 			default:
 				usage(stderr, EXIT_FAILURE);
@@ -727,7 +1022,8 @@  main(int argc, char **argv)
 					directory = optarg;
 				else {
 					fprintf(stderr,
-_("%s: More than one -d option specified\n"),
+						_("%s: More than one -d option"
+						  " specified\n"),
 						progname);
 					return EXIT_FAILURE;
 				}
@@ -737,7 +1033,8 @@  _("%s: More than one -d option specified\n"),
 					lcltime = optarg;
 				else {
 					fprintf(stderr,
-_("%s: More than one -l option specified\n"),
+						_("%s: More than one -l option"
+						  " specified\n"),
 						progname);
 					return EXIT_FAILURE;
 				}
@@ -747,7 +1044,8 @@  _("%s: More than one -l option specified\n"),
 					psxrules = optarg;
 				else {
 					fprintf(stderr,
-_("%s: More than one -p option specified\n"),
+						_("%s: More than one -p option"
+						  " specified\n"),
 						progname);
 					return EXIT_FAILURE;
 				}
@@ -763,22 +1061,15 @@  _("%s: More than one -p option specified\n"),
 				tzdefault = optarg;
 				break;
 			case 'y':
-				if (yitcommand == NULL) {
-					warning(_("-y is obsolescent"));
-					yitcommand = optarg;
-				} else {
-					fprintf(stderr,
-_("%s: More than one -y option specified\n"),
-						progname);
-					return EXIT_FAILURE;
-				}
+				warning(_("-y ignored"));
 				break;
 			case 'L':
 				if (leapsec == NULL)
 					leapsec = optarg;
 				else {
 					fprintf(stderr,
-_("%s: More than one -L option specified\n"),
+						_("%s: More than one -L option"
+						  " specified\n"),
 						progname);
 					return EXIT_FAILURE;
 				}
@@ -789,44 +1080,64 @@  _("%s: More than one -L option specified\n"),
 			case 'r':
 				if (timerange_given) {
 				  fprintf(stderr,
-_("%s: More than one -r option specified\n"),
+					  _("%s: More than one -r option"
+					    " specified\n"),
 					  progname);
 				  return EXIT_FAILURE;
 				}
 				if (! timerange_option(optarg)) {
 				  fprintf(stderr,
-_("%s: invalid time range: %s\n"),
+					  _("%s: invalid time range: %s\n"),
 					  progname, optarg);
 				  return EXIT_FAILURE;
 				}
 				timerange_given = true;
 				break;
+			case 'R':
+				if (! redundant_time_option(optarg)) {
+				  fprintf(stderr, _("%s: invalid time: %s\n"),
+					  progname, optarg);
+				  return EXIT_FAILURE;
+				}
+				break;
 			case 's':
 				warning(_("-s ignored"));
 				break;
 		}
 	if (optind == argc - 1 && strcmp(argv[optind], "=") == 0)
 		usage(stderr, EXIT_FAILURE);	/* usage message by request */
-	if (bloat == 0)
-	  bloat = strcmp(ZIC_BLOAT_DEFAULT, "slim") == 0 ? -1 : 1;
+	if (hi_time + (hi_time < ZIC_MAX) < redundant_time) {
+	  fprintf(stderr, _("%s: -R time exceeds -r cutoff\n"), progname);
+	  return EXIT_FAILURE;
+	}
+	if (redundant_time < lo_time)
+	  redundant_time = lo_time;
+	if (bloat == 0) {
+	  static char const bloat_default[] = ZIC_BLOAT_DEFAULT;
+	  if (strcmp(bloat_default, "slim") == 0)
+	    bloat = -1;
+	  else if (strcmp(bloat_default, "fat") == 0)
+	    bloat = 1;
+	  else
+	    abort(); /* Configuration error.  */
+	}
 	if (directory == NULL)
 		directory = TZDIR;
 	if (tzdefault == NULL)
 		tzdefault = TZDEFAULT;
-	if (yitcommand == NULL)
-		yitcommand = "yearistype";
 
 	if (optind < argc && leapsec != NULL) {
-		infile(leapsec);
+		infile(LEAPSEC_FILENUM, leapsec);
 		adjleap();
 	}
 
 	for (k = optind; k < argc; k++)
-		infile(argv[k]);
+	  infile(k, argv[k]);
 	if (errors)
 		return EXIT_FAILURE;
 	associate();
 	change_directory(directory);
+	catch_signals();
 	for (i = 0; i < nzones; i = j) {
 		/*
 		** Find the next non-continuation zone entry.
@@ -835,24 +1146,13 @@  _("%s: invalid time range: %s\n"),
 			continue;
 		outzone(&zones[i], j - i);
 	}
-	/*
-	** Make links.
-	*/
-	for (i = 0; i < nlinks; ++i) {
-		eat(links[i].l_filename, links[i].l_linenum);
-		dolink(links[i].l_from, links[i].l_to, false);
-		if (noise)
-			for (j = 0; j < nlinks; ++j)
-				if (strcmp(links[i].l_to,
-					links[j].l_from) == 0)
-						warning(_("link to link"));
-	}
+	make_links();
 	if (lcltime != NULL) {
-		eat(_("command line"), 1);
+		eat(COMMAND_LINE_FILENUM, 1);
 		dolink(lcltime, tzdefault, true);
 	}
 	if (psxrules != NULL) {
-		eat(_("command line"), 1);
+		eat(COMMAND_LINE_FILENUM, 1);
 		dolink(psxrules, TZDEFRULES, true);
 	}
 	if (warnings && (ferror(stderr) || fclose(stderr) != 0))
@@ -868,9 +1168,9 @@  componentcheck(char const *name, char const *component,
 	ptrdiff_t component_len = component_end - component;
 	if (component_len == 0) {
 	  if (!*name)
-	    error (_("empty file name"));
+	    error(_("empty file name"));
 	  else
-	    error (_(component == name
+	    error(_(component == name
 		     ? "file name '%s' begins with '/'"
 		     : *component_end
 		     ? "file name '%s' contains '//'"
@@ -931,34 +1231,197 @@  namecheck(const char *name)
 	return componentcheck(name, component, cp);
 }
 
-/* Create symlink contents suitable for symlinking FROM to TO, as a
-   freshly allocated string.  FROM should be a relative file name, and
-   is relative to the global variable DIRECTORY.  TO can be either
+/* Return a random uint_fast64_t.  */
+static uint_fast64_t
+get_rand_u64(void)
+{
+#if HAVE_GETRANDOM
+  static uint_fast64_t entropy_buffer[max(1, 256 / sizeof(uint_fast64_t))];
+  static int nwords;
+  if (!nwords) {
+    ssize_t s;
+    do
+      s = getrandom(entropy_buffer, sizeof entropy_buffer, 0);
+    while (s < 0 && errno == EINTR);
+
+    nwords = s < 0 ? -1 : s / sizeof *entropy_buffer;
+  }
+  if (0 < nwords)
+    return entropy_buffer[--nwords];
+#endif
+
+  /* getrandom didn't work, so fall back on portable code that is
+     not the best because the seed isn't cryptographically random and
+     'rand' might not be cryptographically secure.  */
+  {
+    static bool initialized;
+    if (!initialized) {
+      srand(time(NULL));
+      initialized = true;
+    }
+  }
+
+  /* Return a random number if rand() yields a random number and in
+     the typical case where RAND_MAX is one less than a power of two.
+     In other cases this code yields a sort-of-random number.  */
+  {
+    uint_fast64_t rand_max = RAND_MAX,
+      nrand = rand_max < UINT_FAST64_MAX ? rand_max + 1 : 0,
+      rmod = INT_MAX < UINT_FAST64_MAX ? 0 : UINT_FAST64_MAX / nrand + 1,
+      r = 0, rmax = 0;
+
+    do {
+      uint_fast64_t rmax1 = rmax;
+      if (rmod) {
+	/* Avoid signed integer overflow on theoretical platforms
+	   where uint_fast64_t promotes to int.  */
+	rmax1 %= rmod;
+	r %= rmod;
+      }
+      rmax1 = nrand * rmax1 + rand_max;
+      r = nrand * r + rand();
+      rmax = rmax < rmax1 ? rmax1 : UINT_FAST64_MAX;
+    } while (rmax < UINT_FAST64_MAX);
+
+    return r;
+  }
+}
+
+/* Generate a randomish name in the same directory as *NAME.  If
+   *NAMEALLOC, put the name into *NAMEALLOC which is assumed to be
+   that returned by a previous call and is thus already almost set up
+   and equal to *NAME; otherwise, allocate a new name and put its
+   address into both *NAMEALLOC and *NAME.  */
+static void
+random_dirent(char const **name, char **namealloc)
+{
+  char const *src = *name;
+  char *dst = *namealloc;
+  static char const prefix[] = ".zic";
+  static char const alphabet[] =
+    "abcdefghijklmnopqrstuvwxyz"
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    "0123456789";
+  enum { prefixlen = sizeof prefix - 1, alphabetlen = sizeof alphabet - 1 };
+  int suffixlen = 6;
+  char const *lastslash = strrchr(src, '/');
+  ptrdiff_t dirlen = lastslash ? lastslash + 1 - src : 0;
+  int i;
+  uint_fast64_t r;
+  uint_fast64_t base = alphabetlen;
+
+  /* BASE**6 */
+  uint_fast64_t base__6 = base * base * base * base * base * base;
+
+  /* The largest uintmax_t that is a multiple of BASE**6.  Any random
+     uintmax_t value that is this value or greater, yields a biased
+     remainder when divided by BASE**6.  UNFAIR_MIN equals the
+     mathematical value of ((UINTMAX_MAX + 1) - (UINTMAX_MAX + 1) % BASE**6)
+     computed without overflow.  */
+  uint_fast64_t unfair_min = - ((UINTMAX_MAX % base__6 + 1) % base__6);
+
+  if (!dst) {
+    dst = emalloc(size_sum(dirlen, prefixlen + suffixlen + 1));
+    memcpy(dst, src, dirlen);
+    memcpy(dst + dirlen, prefix, prefixlen);
+    dst[dirlen + prefixlen + suffixlen] = '\0';
+    *name = *namealloc = dst;
+  }
+
+  do
+    r = get_rand_u64();
+  while (unfair_min <= r);
+
+  for (i = 0; i < suffixlen; i++) {
+    dst[dirlen + prefixlen + i] = alphabet[r % alphabetlen];
+    r /= alphabetlen;
+  }
+}
+
+/* Prepare to write to the file *OUTNAME, using *TEMPNAME to store the
+   name of the temporary file that will eventually be renamed to
+   *OUTNAME.  Assign the temporary file's name to both *OUTNAME and
+   *TEMPNAME.  If *TEMPNAME is null, allocate the name of any such
+   temporary file; otherwise, reuse *TEMPNAME's storage, which is
+   already set up and only needs its trailing suffix updated.  */
+static FILE *
+open_outfile(char const **outname, char **tempname)
+{
+#if __STDC_VERSION__ < 201112
+  static char const fopen_mode[] = "wb";
+#else
+  static char const fopen_mode[] = "wbx";
+#endif
+
+  FILE *fp;
+  bool dirs_made = false;
+  if (!*tempname)
+    random_dirent(outname, tempname);
+
+  while (! (fp = fopen(*outname, fopen_mode))) {
+    int fopen_errno = errno;
+    if (fopen_errno == ENOENT && !dirs_made) {
+      mkdirs(*outname, true);
+      dirs_made = true;
+    } else if (fopen_errno == EEXIST)
+      random_dirent(outname, tempname);
+    else {
+      fprintf(stderr, _("%s: Can't create %s/%s: %s\n"),
+	      progname, directory, *outname, strerror(fopen_errno));
+      exit(EXIT_FAILURE);
+    }
+  }
+
+  return fp;
+}
+
+/* If TEMPNAME, the result is in the temporary file TEMPNAME even
+   though the user wanted it in NAME, so rename TEMPNAME to NAME.
+   Report an error and exit if there is trouble.  Also, free TEMPNAME.  */
+static void
+rename_dest(char *tempname, char const *name)
+{
+  if (tempname) {
+    if (rename(tempname, name) != 0) {
+      int rename_errno = errno;
+      remove(tempname);
+      fprintf(stderr, _("%s: rename to %s/%s: %s\n"),
+	      progname, directory, name, strerror(rename_errno));
+      exit(EXIT_FAILURE);
+    }
+    free(tempname);
+  }
+}
+
+/* Create symlink contents suitable for symlinking TARGET to LINKNAME, as a
+   freshly allocated string.  TARGET should be a relative file name, and
+   is relative to the global variable DIRECTORY.  LINKNAME can be either
    relative or absolute.  */
 static char *
-relname(char const *from, char const *to)
+relname(char const *target, char const *linkname)
 {
-  size_t i, taillen, dotdotetcsize;
-  size_t dir_len = 0, dotdots = 0, linksize = SIZE_MAX;
-  char const *f = from;
+  size_t i, taillen, dir_len = 0, dotdots = 0;
+  ptrdiff_t dotdotetcsize, linksize = INDEX_MAX;
+  char const *f = target;
   char *result = NULL;
-  if (*to == '/') {
+  if (*linkname == '/') {
     /* Make F absolute too.  */
     size_t len = strlen(directory);
-    bool needslash = len && directory[len - 1] != '/';
-    linksize = len + needslash + strlen(from) + 1;
+    size_t lenslash = len + (len && directory[len - 1] != '/');
+    size_t targetsize = strlen(target) + 1;
+    linksize = size_sum(lenslash, targetsize);
     f = result = emalloc(linksize);
-    strcpy(result, directory);
+    memcpy(result, directory, len);
     result[len] = '/';
-    strcpy(result + len + needslash, from);
+    memcpy(result + lenslash, target, targetsize);
   }
-  for (i = 0; f[i] && f[i] == to[i]; i++)
+  for (i = 0; f[i] && f[i] == linkname[i]; i++)
     if (f[i] == '/')
       dir_len = i + 1;
-  for (; to[i]; i++)
-    dotdots += to[i] == '/' && to[i - 1] != '/';
+  for (; linkname[i]; i++)
+    dotdots += linkname[i] == '/' && linkname[i - 1] != '/';
   taillen = strlen(f + dir_len);
-  dotdotetcsize = 3 * dotdots + taillen + 1;
+  dotdotetcsize = size_sum(size_product(dotdots, 3), taillen + 1);
   if (dotdotetcsize <= linksize) {
     if (!result)
       result = emalloc(dotdotetcsize);
@@ -969,83 +1432,124 @@  relname(char const *from, char const *to)
   return result;
 }
 
-/* Hard link FROM to TO, following any symbolic links.
-   Return 0 if successful, an error number otherwise.  */
-static int
-hardlinkerr(char const *from, char const *to)
+/* Return true if A and B must have the same parent dir if A and B exist.
+   Return false if this is not necessarily true (though it might be true).
+   Keep it simple, and do not inspect the file system.  */
+static bool
+same_parent_dirs(char const *a, char const *b)
 {
-  int r = linkat(AT_FDCWD, from, AT_FDCWD, to, AT_SYMLINK_FOLLOW);
-  return r == 0 ? 0 : errno;
+  for (; *a == *b; a++, b++)
+    if (!*a)
+      return true;
+  return ! (strchr(a, '/') || strchr(b, '/'));
 }
 
 static void
-dolink(char const *fromfield, char const *tofield, bool staysymlink)
+dolink(char const *target, char const *linkname, bool staysymlink)
 {
-	bool todirs_made = false;
+	bool linkdirs_made = false;
 	int link_errno;
+	char *tempname = NULL;
+	char const *outname = linkname;
+	int targetissym = -2, linknameissym = -2;
 
-	/*
-	** We get to be careful here since
-	** there's a fair chance of root running us.
-	*/
-	if (itsdir(fromfield)) {
-		fprintf(stderr, _("%s: link from %s/%s failed: %s\n"),
-			progname, directory, fromfield, strerror(EPERM));
-		exit(EXIT_FAILURE);
-	}
-	if (staysymlink)
-	  staysymlink = itssymlink(tofield);
-	if (remove(tofield) == 0)
-	  todirs_made = true;
-	else if (errno != ENOENT) {
-	  char const *e = strerror(errno);
-	  fprintf(stderr, _("%s: Can't remove %s/%s: %s\n"),
-		  progname, directory, tofield, e);
-	  exit(EXIT_FAILURE);
+	check_for_signal();
+
+	if (strcmp(target, "-") == 0) {
+	  if (remove(linkname) == 0 || errno == ENOENT || errno == ENOTDIR)
+	    return;
+	  else {
+	    char const *e = strerror(errno);
+	    fprintf(stderr, _("%s: Can't remove %s/%s: %s\n"),
+		    progname, directory, linkname, e);
+	    exit(EXIT_FAILURE);
+	  }
 	}
-	link_errno = staysymlink ? ENOTSUP : hardlinkerr(fromfield, tofield);
-	if (link_errno == ENOENT && !todirs_made) {
-	  mkdirs(tofield, true);
-	  todirs_made = true;
-	  link_errno = hardlinkerr(fromfield, tofield);
+
+	while (true) {
+	  if (linkat(AT_FDCWD, target, AT_FDCWD, outname, AT_SYMLINK_FOLLOW)
+	      == 0) {
+	    link_errno = 0;
+	    break;
+	  }
+	  link_errno = errno;
+	  /* Linux 2.6.16 and 2.6.17 mishandle AT_SYMLINK_FOLLOW.  */
+	  if (link_errno == EINVAL)
+	    link_errno = ENOTSUP;
+#if HAVE_LINK
+	  /* If linkat is not supported, fall back on link(A, B).
+	     However, skip this if A is a relative symlink
+	     and A and B might not have the same parent directory.
+	     On some platforms link(A, B) does not follow a symlink A,
+	     and if A is relative it might misbehave elsewhere.  */
+	  if (link_errno == ENOTSUP
+	      && (same_parent_dirs(target, outname)
+		  || 0 <= itssymlink(target, &targetissym))) {
+	    if (link(target, outname) == 0) {
+	      link_errno = 0;
+	      break;
+	    }
+	    link_errno = errno;
+	  }
+#endif
+	  if (link_errno == EXDEV || link_errno == ENOTSUP)
+	    break;
+
+	  if (link_errno == EEXIST) {
+	    staysymlink &= !tempname;
+	    random_dirent(&outname, &tempname);
+	    if (staysymlink && itssymlink(linkname, &linknameissym))
+	      break;
+	  } else if (link_errno == ENOENT && !linkdirs_made) {
+	    mkdirs(linkname, true);
+	    linkdirs_made = true;
+	  } else {
+	    fprintf(stderr, _("%s: Can't link %s/%s to %s/%s: %s\n"),
+		    progname, directory, target, directory, outname,
+		    strerror(link_errno));
+	    exit(EXIT_FAILURE);
+	  }
 	}
 	if (link_errno != 0) {
-	  bool absolute = *fromfield == '/';
-	  char *linkalloc = absolute ? NULL : relname(fromfield, tofield);
-	  char const *contents = absolute ? fromfield : linkalloc;
-	  int symlink_errno = symlink(contents, tofield) == 0 ? 0 : errno;
-	  if (!todirs_made
-	      && (symlink_errno == ENOENT || symlink_errno == ENOTSUP)) {
-	    mkdirs(tofield, true);
-	    if (symlink_errno == ENOENT)
-	      symlink_errno = symlink(contents, tofield) == 0 ? 0 : errno;
+	  bool absolute = *target == '/';
+	  char *linkalloc = absolute ? NULL : relname(target, linkname);
+	  char const *contents = absolute ? target : linkalloc;
+	  int symlink_errno;
+
+	  while (true) {
+	    if (symlink(contents, outname) == 0) {
+	      symlink_errno = 0;
+	      break;
+	    }
+	    symlink_errno = errno;
+	    if (symlink_errno == EEXIST)
+	      random_dirent(&outname, &tempname);
+	    else if (symlink_errno == ENOENT && !linkdirs_made) {
+	      mkdirs(linkname, true);
+	      linkdirs_made = true;
+	    } else
+	      break;
 	  }
 	  free(linkalloc);
 	  if (symlink_errno == 0) {
-	    if (link_errno != ENOTSUP)
+	    if (link_errno != ENOTSUP && link_errno != EEXIST)
 	      warning(_("symbolic link used because hard link failed: %s"),
 		      strerror(link_errno));
 	  } else {
 	    FILE *fp, *tp;
 	    int c;
-	    fp = fopen(fromfield, "rb");
+	    fp = fopen(target, "rb");
 	    if (!fp) {
 	      char const *e = strerror(errno);
 	      fprintf(stderr, _("%s: Can't read %s/%s: %s\n"),
-		      progname, directory, fromfield, e);
-	      exit(EXIT_FAILURE);
-	    }
-	    tp = fopen(tofield, "wb");
-	    if (!tp) {
-	      char const *e = strerror(errno);
-	      fprintf(stderr, _("%s: Can't create %s/%s: %s\n"),
-		      progname, directory, tofield, e);
+		      progname, directory, target, e);
 	      exit(EXIT_FAILURE);
 	    }
+	    tp = open_outfile(&outname, &tempname);
 	    while ((c = getc(fp)) != EOF)
 	      putc(c, tp);
-	    close_file(fp, directory, fromfield);
-	    close_file(tp, directory, tofield);
+	    close_file(tp, directory, linkname, tempname);
+	    close_file(fp, directory, target, NULL);
 	    if (link_errno != ENOTSUP)
 	      warning(_("copy used because hard link failed: %s"),
 		      strerror(link_errno));
@@ -1054,37 +1558,20 @@  dolink(char const *fromfield, char const *tofield, bool staysymlink)
 		      strerror(symlink_errno));
 	  }
 	}
+	rename_dest(tempname, linkname);
 }
 
-/* Return true if NAME is a directory.  */
-static bool
-itsdir(char const *name)
-{
-	struct stat st;
-	int res = stat(name, &st);
-#ifdef S_ISDIR
-	if (res == 0)
-		return S_ISDIR(st.st_mode) != 0;
-#endif
-	if (res == 0 || errno == EOVERFLOW) {
-		size_t n = strlen(name);
-		char *nameslashdot = emalloc(n + 3);
-		bool dir;
-		memcpy(nameslashdot, name, n);
-		strcpy(&nameslashdot[n], &"/."[! (n && name[n - 1] != '/')]);
-		dir = stat(nameslashdot, &st) == 0 || errno == EOVERFLOW;
-		free(nameslashdot);
-		return dir;
-	}
-	return false;
-}
-
-/* Return true if NAME is a symbolic link.  */
-static bool
-itssymlink(char const *name)
+/* Return 1 if NAME is an absolute symbolic link, -1 if it is relative,
+   0 if it is not a symbolic link.  If *CACHE is not -2, it is the
+   cached result of a previous call to this function with the same NAME.  */
+static int
+itssymlink(char const *name, int *cache)
 {
-  char c;
-  return 0 <= readlink(name, &c, 1);
+  if (*cache == -2) {
+    char c = '\0';
+    *cache = readlink(name, &c, 1) < 0 ? 0 : c == '/' ? 1 : -1;
+  }
+  return *cache;
 }
 
 /*
@@ -1098,8 +1585,8 @@  itssymlink(char const *name)
 static int
 rcomp(const void *cp1, const void *cp2)
 {
-	return strcmp(((const struct rule *) cp1)->r_name,
-		((const struct rule *) cp2)->r_name);
+  struct rule const *r1 = cp1, *r2 = cp2;
+  return strcmp(r1->r_name, r2->r_name);
 }
 
 static void
@@ -1109,28 +1596,26 @@  associate(void)
 	register struct rule *	rp;
 	register ptrdiff_t i, j, base, out;
 
-	if (nrules != 0) {
+	if (1 < nrules) {
 		qsort(rules, nrules, sizeof *rules, rcomp);
 		for (i = 0; i < nrules - 1; ++i) {
 			if (strcmp(rules[i].r_name,
 				rules[i + 1].r_name) != 0)
 					continue;
-			if (strcmp(rules[i].r_filename,
-				rules[i + 1].r_filename) == 0)
+			if (rules[i].r_filenum == rules[i + 1].r_filenum)
 					continue;
-			eat(rules[i].r_filename, rules[i].r_linenum);
+			eat(rules[i].r_filenum, rules[i].r_linenum);
 			warning(_("same rule name in multiple files"));
-			eat(rules[i + 1].r_filename, rules[i + 1].r_linenum);
+			eat(rules[i + 1].r_filenum, rules[i + 1].r_linenum);
 			warning(_("same rule name in multiple files"));
 			for (j = i + 2; j < nrules; ++j) {
 				if (strcmp(rules[i].r_name,
 					rules[j].r_name) != 0)
 						break;
-				if (strcmp(rules[i].r_filename,
-					rules[j].r_filename) == 0)
+				if (rules[i].r_filenum == rules[j].r_filenum)
 						continue;
-				if (strcmp(rules[i + 1].r_filename,
-					rules[j].r_filename) == 0)
+				if (rules[i + 1].r_filenum
+				    == rules[j].r_filenum)
 						continue;
 				break;
 			}
@@ -1161,7 +1646,7 @@  associate(void)
 			/*
 			** Maybe we have a local standard time offset.
 			*/
-			eat(zp->z_filename, zp->z_linenum);
+			eat(zp->z_filenum, zp->z_linenum);
 			zp->z_save = getsave(zp->z_rule, &zp->z_isdst);
 			/*
 			** Note, though, that if there's no rule,
@@ -1175,20 +1660,48 @@  associate(void)
 		exit(EXIT_FAILURE);
 }
 
+/* Read a text line from FP into BUF, which is of size BUFSIZE.
+   Terminate it with a NUL byte instead of a newline.
+   Return true if successful, false if EOF.
+   On error, report the error and exit.  */
+static bool
+inputline(FILE *fp, char *buf, ptrdiff_t bufsize)
+{
+  ptrdiff_t linelen = 0, ch;
+  while ((ch = getc(fp)) != '\n') {
+    if (ch < 0) {
+      if (ferror(fp)) {
+	error(_("input error"));
+	exit(EXIT_FAILURE);
+      }
+      if (linelen == 0)
+	return false;
+      error(_("unterminated line"));
+      exit(EXIT_FAILURE);
+    }
+    if (!ch) {
+      error(_("NUL input byte"));
+      exit(EXIT_FAILURE);
+    }
+    buf[linelen++] = ch;
+    if (linelen == bufsize) {
+      error(_("line too long"));
+      exit(EXIT_FAILURE);
+    }
+  }
+  buf[linelen] = '\0';
+  return true;
+}
+
 static void
-infile(const char *name)
+infile(int fnum, char const *name)
 {
 	register FILE *			fp;
-	register char **		fields;
-	register char *			cp;
 	register const struct lookup *	lp;
-	register int			nfields;
 	register bool			wantcont;
 	register lineno			num;
-	char				buf[BUFSIZ];
 
 	if (strcmp(name, "-") == 0) {
-		name = _("standard input");
 		fp = stdin;
 	} else if ((fp = fopen(name, "r")) == NULL) {
 		const char *e = strerror(errno);
@@ -1199,32 +1712,23 @@  infile(const char *name)
 	}
 	wantcont = false;
 	for (num = 1; ; ++num) {
-		eat(name, num);
-		if (fgets(buf, sizeof buf, fp) != buf)
-			break;
-		cp = strchr(buf, '\n');
-		if (cp == NULL) {
-			error(_("line too long"));
-			exit(EXIT_FAILURE);
-		}
-		*cp = '\0';
-		fields = getfields(buf);
-		nfields = 0;
-		while (fields[nfields] != NULL) {
-			static char	nada;
-
-			if (strcmp(fields[nfields], "-") == 0)
-				fields[nfields] = &nada;
-			++nfields;
-		}
+		enum { bufsize_bound
+		  = (min(INT_MAX, INDEX_MAX) / FORMAT_LEN_GROWTH_BOUND) };
+		char buf[min(_POSIX2_LINE_MAX, bufsize_bound)];
+		int nfields;
+		char *fields[MAX_FIELDS];
+		eat(fnum, num);
+		if (!inputline(fp, buf, sizeof buf))
+		  break;
+		nfields = getfields(buf, fields,
+				    sizeof fields / sizeof *fields);
 		if (nfields == 0) {
-		  if (name == leapsec && *buf == '#')
-		    sscanf(buf, "#expires %"SCNdZIC, &comment_leapexpires);
+			/* nothing to do */
 		} else if (wantcont) {
 			wantcont = inzcont(fields, nfields);
 		} else {
 			struct lookup const *line_codes
-			  = name == leapsec ? leap_line_codes : zi_line_codes;
+			  = fnum < 0 ? leap_line_codes : zi_line_codes;
 			lp = byword(fields[0], line_codes);
 			if (lp == NULL)
 				error(_("input line of unknown type"));
@@ -1248,16 +1752,11 @@  infile(const char *name)
 					inexpires(fields, nfields);
 					wantcont = false;
 					break;
-				default:	/* "cannot happen" */
-					fprintf(stderr,
-_("%s: panic: Invalid l_value %d\n"),
-						progname, lp->l_value);
-					exit(EXIT_FAILURE);
+				default: unreachable();
 			}
 		}
-		free(fields);
 	}
-	close_file(fp, NULL, filename);
+	close_file(fp, NULL, filename(fnum), NULL);
 	if (wantcont)
 		error(_("expected continuation line not found"));
 }
@@ -1291,15 +1790,15 @@  gethms(char const *string, char const *errstring)
 	  default: ok = false; break;
 	  case 8:
 	    ok = '0' <= xr && xr <= '9';
-	    /* fallthrough */
+	    ATTRIBUTE_FALLTHROUGH;
 	  case 7:
 	    ok &= ssx == '.';
 	    if (ok && noise)
 	      warning(_("fractional seconds rejected by"
 			" pre-2018 versions of zic"));
-	    /* fallthrough */
-	  case 5: ok &= mmx == ':'; /* fallthrough */
-	  case 3: ok &= hhx == ':'; /* fallthrough */
+	    ATTRIBUTE_FALLTHROUGH;
+	  case 5: ok &= mmx == ':'; ATTRIBUTE_FALLTHROUGH;
+	  case 3: ok &= hhx == ':'; ATTRIBUTE_FALLTHROUGH;
 	  case 1: break;
 	}
 	if (!ok) {
@@ -1329,7 +1828,7 @@  getsave(char *field, bool *isdst)
 {
   int dst = -1;
   zic_t save;
-  size_t fieldlen = strlen(field);
+  ptrdiff_t fieldlen = strlen(field);
   if (fieldlen != 0) {
     char *ep = field + fieldlen - 1;
     switch (*ep) {
@@ -1345,7 +1844,7 @@  getsave(char *field, bool *isdst)
 static void
 inrule(char **fields, int nfields)
 {
-	static struct rule	r;
+	struct rule r;
 
 	if (nfields != RULE_FIELDS) {
 		error(_("wrong number of fields on Rule line"));
@@ -1360,13 +1859,15 @@  inrule(char **fields, int nfields)
 		error(_("Invalid rule name \"%s\""), fields[RF_NAME]);
 		return;
 	}
-	r.r_filename = filename;
+	r.r_filenum = filenum;
 	r.r_linenum = linenum;
 	r.r_save = getsave(fields[RF_SAVE], &r.r_isdst);
-	rulesub(&r, fields[RF_LOYEAR], fields[RF_HIYEAR], fields[RF_COMMAND],
-		fields[RF_MONTH], fields[RF_DAY], fields[RF_TOD]);
-	r.r_name = ecpyalloc(fields[RF_NAME]);
-	r.r_abbrvar = ecpyalloc(fields[RF_ABBRVAR]);
+	if (!rulesub(&r, fields[RF_LOYEAR], fields[RF_HIYEAR],
+		     fields[RF_COMMAND], fields[RF_MONTH], fields[RF_DAY],
+		     fields[RF_TOD]))
+	  return;
+	r.r_name = estrdup(fields[RF_NAME]);
+	r.r_abbrvar = estrdup(fields[RF_ABBRVAR]);
 	if (max_abbrvar_len < strlen(r.r_abbrvar))
 		max_abbrvar_len = strlen(r.r_abbrvar);
 	rules = growalloc(rules, sizeof *rules, nrules, &nrules_alloc);
@@ -1383,25 +1884,23 @@  inzone(char **fields, int nfields)
 		return false;
 	}
 	if (lcltime != NULL && strcmp(fields[ZF_NAME], tzdefault) == 0) {
-		error(
-_("\"Zone %s\" line and -l option are mutually exclusive"),
-			tzdefault);
-		return false;
+	  error(_("\"Zone %s\" line and -l option are mutually exclusive"),
+		tzdefault);
+	  return false;
 	}
 	if (strcmp(fields[ZF_NAME], TZDEFRULES) == 0 && psxrules != NULL) {
-		error(
-_("\"Zone %s\" line and -p option are mutually exclusive"),
-			TZDEFRULES);
-		return false;
+	  error(_("\"Zone %s\" line and -p option are mutually exclusive"),
+		TZDEFRULES);
+	  return false;
 	}
 	for (i = 0; i < nzones; ++i)
 		if (zones[i].z_name != NULL &&
 			strcmp(zones[i].z_name, fields[ZF_NAME]) == 0) {
 				error(_("duplicate zone name %s"
 					" (file \"%s\", line %"PRIdMAX")"),
-					fields[ZF_NAME],
-					zones[i].z_filename,
-					zones[i].z_linenum);
+				      fields[ZF_NAME],
+				      filename(zones[i].z_filenum),
+				      zones[i].z_linenum);
 				return false;
 		}
 	return inzsub(fields, nfields, false);
@@ -1422,7 +1921,8 @@  inzsub(char **fields, int nfields, bool iscont)
 {
 	register char *		cp;
 	char *			cp1;
-	static struct zone	z;
+	struct zone z;
+	int format_len;
 	register int		i_stdoff, i_rule, i_format;
 	register int		i_untilyear, i_untilmonth;
 	register int		i_untilday, i_untiltime;
@@ -1436,7 +1936,6 @@  inzsub(char **fields, int nfields, bool iscont)
 		i_untilmonth = ZFC_TILMONTH;
 		i_untilday = ZFC_TILDAY;
 		i_untiltime = ZFC_TILTIME;
-		z.z_name = NULL;
 	} else if (!namecheck(fields[ZF_NAME]))
 		return false;
 	else {
@@ -1447,9 +1946,8 @@  inzsub(char **fields, int nfields, bool iscont)
 		i_untilmonth = ZF_TILMONTH;
 		i_untilday = ZF_TILDAY;
 		i_untiltime = ZF_TILTIME;
-		z.z_name = ecpyalloc(fields[ZF_NAME]);
 	}
-	z.z_filename = filename;
+	z.z_filenum = filenum;
 	z.z_linenum = linenum;
 	z.z_stdoff = gethms(fields[i_stdoff], _("invalid UT offset"));
 	if ((cp = strchr(fields[i_format], '%')) != 0) {
@@ -1459,29 +1957,24 @@  inzsub(char **fields, int nfields, bool iscont)
 			return false;
 		}
 	}
-	z.z_rule = ecpyalloc(fields[i_rule]);
-	z.z_format = cp1 = ecpyalloc(fields[i_format]);
 	z.z_format_specifier = cp ? *cp : '\0';
-	if (z.z_format_specifier == 'z') {
-	  if (noise)
-	    warning(_("format '%s' not handled by pre-2015 versions of zic"),
-		    z.z_format);
-	  cp1[cp - fields[i_format]] = 's';
-	}
-	if (max_format_len < strlen(z.z_format))
-		max_format_len = strlen(z.z_format);
+	format_len = strlen(fields[i_format]);
+	if (max_format_len < format_len)
+	  max_format_len = format_len;
 	hasuntil = nfields > i_untilyear;
 	if (hasuntil) {
-		z.z_untilrule.r_filename = filename;
+		z.z_untilrule.r_filenum = filenum;
 		z.z_untilrule.r_linenum = linenum;
-		rulesub(&z.z_untilrule,
+		if (!rulesub(
+			&z.z_untilrule,
 			fields[i_untilyear],
 			"only",
 			"",
 			(nfields > i_untilmonth) ?
 			fields[i_untilmonth] : "Jan",
 			(nfields > i_untilday) ? fields[i_untilday] : "1",
-			(nfields > i_untiltime) ? fields[i_untiltime] : "0");
+			(nfields > i_untiltime) ? fields[i_untiltime] : "0"))
+		  return false;
 		z.z_untiltime = rpytime(&z.z_untilrule,
 			z.z_untilrule.r_loyear);
 		if (iscont && nzones > 0 &&
@@ -1490,12 +1983,20 @@  inzsub(char **fields, int nfields, bool iscont)
 			zones[nzones - 1].z_untiltime > min_time &&
 			zones[nzones - 1].z_untiltime < max_time &&
 			zones[nzones - 1].z_untiltime >= z.z_untiltime) {
-				error(_(
-"Zone continuation line end time is not after end time of previous line"
-					));
-				return false;
+		  error(_("Zone continuation line end time is"
+			  " not after end time of previous line"));
+		  return false;
 		}
 	}
+	z.z_name = iscont ? NULL : estrdup(fields[ZF_NAME]);
+	z.z_rule = estrdup(fields[i_rule]);
+	z.z_format = cp1 = estrdup(fields[i_format]);
+	if (z.z_format_specifier == 'z') {
+	  cp1[cp - fields[i_format]] = 's';
+	  if (noise)
+	    warning(_("format '%s' not handled by pre-2015 versions of zic"),
+		    fields[i_format]);
+	}
 	zones = growalloc(zones, sizeof *zones, nzones, &nzones_alloc);
 	zones[nzones++] = z;
 	/*
@@ -1506,7 +2007,7 @@  inzsub(char **fields, int nfields, bool iscont)
 }
 
 static zic_t
-getleapdatetime(char **fields, int nfields, bool expire_line)
+getleapdatetime(char **fields, bool expire_line)
 {
 	register const char *		cp;
 	register const struct lookup *	lp;
@@ -1584,7 +2085,7 @@  inleap(char **fields, int nfields)
   if (nfields != LEAP_FIELDS)
     error(_("wrong number of fields on Leap line"));
   else {
-    zic_t t = getleapdatetime(fields, nfields, false);
+    zic_t t = getleapdatetime(fields, false);
     if (0 <= t) {
       struct lookup const *lp = byword(fields[LP_ROLL], leap_types);
       if (!lp)
@@ -1612,7 +2113,7 @@  inexpires(char **fields, int nfields)
   else if (0 <= leapexpires)
     error(_("multiple Expires lines"));
   else
-    leapexpires = getleapdatetime(fields, nfields, true);
+    leapexpires = getleapdatetime(fields, true);
 }
 
 static void
@@ -1624,21 +2125,21 @@  inlink(char **fields, int nfields)
 		error(_("wrong number of fields on Link line"));
 		return;
 	}
-	if (*fields[LF_FROM] == '\0') {
-		error(_("blank FROM field on Link line"));
+	if (*fields[LF_TARGET] == '\0') {
+		error(_("blank TARGET field on Link line"));
 		return;
 	}
-	if (! namecheck(fields[LF_TO]))
+	if (! namecheck(fields[LF_LINKNAME]))
 	  return;
-	l.l_filename = filename;
+	l.l_filenum = filenum;
 	l.l_linenum = linenum;
-	l.l_from = ecpyalloc(fields[LF_FROM]);
-	l.l_to = ecpyalloc(fields[LF_TO]);
+	l.l_target = estrdup(fields[LF_TARGET]);
+	l.l_linkname = estrdup(fields[LF_LINKNAME]);
 	links = growalloc(links, sizeof *links, nlinks, &nlinks_alloc);
 	links[nlinks++] = l;
 }
 
-static void
+static bool
 rulesub(struct rule *rp, const char *loyearp, const char *hiyearp,
 	const char *typep, const char *monthp, const char *dayp,
 	const char *timep)
@@ -1651,12 +2152,12 @@  rulesub(struct rule *rp, const char *loyearp, const char *hiyearp,
 
 	if ((lp = byword(monthp, mon_names)) == NULL) {
 		error(_("invalid month name"));
-		return;
+		return false;
 	}
 	rp->r_month = lp->l_value;
 	rp->r_todisstd = false;
 	rp->r_todisut = false;
-	dp = ecpyalloc(timep);
+	dp = estrdup(timep);
 	if (*dp != '\0') {
 		ep = dp + strlen(dp) - 1;
 		switch (lowerit(*ep)) {
@@ -1686,59 +2187,41 @@  rulesub(struct rule *rp, const char *loyearp, const char *hiyearp,
 	*/
 	cp = loyearp;
 	lp = byword(cp, begin_years);
-	rp->r_lowasnum = lp == NULL;
-	if (!rp->r_lowasnum) switch (lp->l_value) {
+	if (lp) switch (lp->l_value) {
 		case YR_MINIMUM:
-			rp->r_loyear = ZIC_MIN;
+			warning(_("FROM year \"%s\" is obsolete;"
+				  " treated as %d"),
+				cp, YEAR_32BIT_MIN - 1);
+			rp->r_loyear = YEAR_32BIT_MIN - 1;
 			break;
-		case YR_MAXIMUM:
-			rp->r_loyear = ZIC_MAX;
-			break;
-		default:	/* "cannot happen" */
-			fprintf(stderr,
-				_("%s: panic: Invalid l_value %d\n"),
-				progname, lp->l_value);
-			exit(EXIT_FAILURE);
+		default: unreachable();
 	} else if (sscanf(cp, "%"SCNdZIC"%c", &rp->r_loyear, &xs) != 1) {
 		error(_("invalid starting year"));
-		return;
+		return false;
 	}
 	cp = hiyearp;
 	lp = byword(cp, end_years);
 	rp->r_hiwasnum = lp == NULL;
 	if (!rp->r_hiwasnum) switch (lp->l_value) {
-		case YR_MINIMUM:
-			rp->r_hiyear = ZIC_MIN;
-			break;
 		case YR_MAXIMUM:
 			rp->r_hiyear = ZIC_MAX;
 			break;
 		case YR_ONLY:
 			rp->r_hiyear = rp->r_loyear;
 			break;
-		default:	/* "cannot happen" */
-			fprintf(stderr,
-				_("%s: panic: Invalid l_value %d\n"),
-				progname, lp->l_value);
-			exit(EXIT_FAILURE);
+		default: unreachable();
 	} else if (sscanf(cp, "%"SCNdZIC"%c", &rp->r_hiyear, &xs) != 1) {
 		error(_("invalid ending year"));
-		return;
+		return false;
 	}
 	if (rp->r_loyear > rp->r_hiyear) {
 		error(_("starting year greater than ending year"));
-		return;
+		return false;
 	}
-	if (*typep == '\0')
-		rp->r_yrtype = NULL;
-	else {
-		if (rp->r_loyear == rp->r_hiyear) {
-			error(_("typed single year"));
-			return;
-		}
-		warning(_("year type \"%s\" is obsolete; use \"-\" instead"),
+	if (*typep != '\0') {
+		error(_("year type \"%s\" is unsupported; use \"-\" instead"),
 			typep);
-		rp->r_yrtype = ecpyalloc(typep);
+		return false;
 	}
 	/*
 	** Day work.
@@ -1749,7 +2232,7 @@  rulesub(struct rule *rp, const char *loyearp, const char *hiyearp,
 	**	Sun<=20
 	**	Sun>=7
 	*/
-	dp = ecpyalloc(dayp);
+	dp = estrdup(dayp);
 	if ((lp = byword(dp, lasts)) != NULL) {
 		rp->r_dycode = DC_DOWLEQ;
 		rp->r_wday = lp->l_value;
@@ -1768,12 +2251,12 @@  rulesub(struct rule *rp, const char *loyearp, const char *hiyearp,
 			if (*ep++ != '=') {
 				error(_("invalid day of month"));
 				free(dp);
-				return;
+				return false;
 			}
 			if ((lp = byword(dp, wday_names)) == NULL) {
 				error(_("invalid weekday name"));
 				free(dp);
-				return;
+				return false;
 			}
 			rp->r_wday = lp->l_value;
 		}
@@ -1782,36 +2265,37 @@  rulesub(struct rule *rp, const char *loyearp, const char *hiyearp,
 			(rp->r_dayofmonth > len_months[1][rp->r_month])) {
 				error(_("invalid day of month"));
 				free(dp);
-				return;
+				return false;
 		}
 	}
 	free(dp);
+	return true;
 }
 
 static void
-convert(const int_fast32_t val, char *const buf)
+convert(uint_fast32_t val, char *buf)
 {
 	register int	i;
 	register int	shift;
 	unsigned char *const b = (unsigned char *) buf;
 
 	for (i = 0, shift = 24; i < 4; ++i, shift -= 8)
-		b[i] = val >> shift;
+	  b[i] = (val >> shift) & 0xff;
 }
 
 static void
-convert64(const zic_t val, char *const buf)
+convert64(uint_fast64_t val, char *buf)
 {
 	register int	i;
 	register int	shift;
 	unsigned char *const b = (unsigned char *) buf;
 
 	for (i = 0, shift = 56; i < 8; ++i, shift -= 8)
-		b[i] = val >> shift;
+	  b[i] = (val >> shift) & 0xff;
 }
 
 static void
-puttzcode(const int_fast32_t val, FILE *const fp)
+puttzcode(zic_t val, FILE *fp)
 {
 	char	buf[4];
 
@@ -1835,39 +2319,57 @@  puttzcodepass(zic_t val, FILE *fp, int pass)
 static int
 atcomp(const void *avp, const void *bvp)
 {
-	const zic_t	a = ((const struct attype *) avp)->at;
-	const zic_t	b = ((const struct attype *) bvp)->at;
-
-	return (a < b) ? -1 : (a > b);
+  struct attype const *ap = avp, *bp = bvp;
+  zic_t a = ap->at, b = bp->at;
+  return a < b ? -1 : a > b;
 }
 
 struct timerange {
   int defaulttype;
   ptrdiff_t base, count;
   int leapbase, leapcount;
+  bool leapexpiry;
 };
 
 static struct timerange
 limitrange(struct timerange r, zic_t lo, zic_t hi,
 	   zic_t const *ats, unsigned char const *types)
 {
+  /* Omit ordinary transitions < LO.  */
   while (0 < r.count && ats[r.base] < lo) {
     r.defaulttype = types[r.base];
     r.count--;
     r.base++;
   }
-  while (0 < r.leapcount && trans[r.leapbase] < lo) {
+
+  /* Omit as many initial leap seconds as possible, such that the
+     first leap second in the truncated list is <= LO, and is a
+     positive leap second if and only if it has a positive correction.
+     This supports common TZif readers that assume that the first leap
+     second is positive if and only if its correction is positive.  */
+  while (1 < r.leapcount && trans[r.leapbase + 1] <= lo) {
     r.leapcount--;
     r.leapbase++;
   }
+  while (0 < r.leapbase
+	 && ((corr[r.leapbase - 1] < corr[r.leapbase])
+	     != (0 < corr[r.leapbase]))) {
+    r.leapcount++;
+    r.leapbase--;
+  }
+
 
-  if (hi < ZIC_MAX) {
+  /* Omit ordinary and leap second transitions greater than HI + 1.  */
+  if (hi < max_time) {
     while (0 < r.count && hi + 1 < ats[r.base + r.count - 1])
       r.count--;
     while (0 < r.leapcount && hi + 1 < trans[r.leapbase + r.leapcount - 1])
       r.leapcount--;
   }
 
+  /* Determine whether to append an expiration to the leap second table.  */
+  r.leapexpiry = 0 <= leapexpires && leapexpires - 1 <= hi;
+
   return r;
 }
 
@@ -1878,20 +2380,18 @@  writezone(const char *const name, const char *const string, char version,
 	register FILE *			fp;
 	register ptrdiff_t		i, j;
 	register int			pass;
-	static const struct tzhead	tzh0;
-	static struct tzhead		tzh;
-	bool dir_checked = false;
-	zic_t one = 1;
-	zic_t y2038_boundary = one << 31;
-	ptrdiff_t nats = timecnt + WORK_AROUND_QTBUG_53071;
+	char *tempname = NULL;
+	char const *outname = name;
 
 	/* Allocate the ATS and TYPES arrays via a single malloc,
-	   as this is a bit faster.  */
-	zic_t *ats = emalloc(align_to(size_product(nats, sizeof *ats + 1),
-				      _Alignof(zic_t)));
-	void *typesptr = ats + nats;
+	   as this is a bit faster.  Do not malloc(0) if !timecnt,
+	   as that might return NULL even on success.  */
+	zic_t *ats = emalloc(align_to(size_product(timecnt + !timecnt,
+						   sizeof *ats + 1),
+				      alignof(zic_t)));
+	void *typesptr = ats + timecnt;
 	unsigned char *types = typesptr;
-	struct timerange rangeall, range32, range64;
+	struct timerange rangeall = {0}, range32, range64;
 
 	/*
 	** Sort.
@@ -1959,60 +2459,50 @@  writezone(const char *const name, const char *const string, char version,
 			}
 	}
 
-	/* Work around QTBUG-53071 for timestamps less than y2038_boundary - 1,
-	   by inserting a no-op transition at time y2038_boundary - 1.
-	   This works only for timestamps before the boundary, which
-	   should be good enough in practice as QTBUG-53071 should be
-	   long-dead by 2038.  Do this after correcting for leap
-	   seconds, as the idea is to insert a transition just before
-	   32-bit time_t rolls around, and this occurs at a slightly
-	   different moment if transitions are leap-second corrected.  */
-	if (WORK_AROUND_QTBUG_53071 && timecnt != 0 && want_bloat()
-	    && ats[timecnt - 1] < y2038_boundary - 1 && strchr(string, '<')) {
-	  ats[timecnt] = y2038_boundary - 1;
-	  types[timecnt] = types[timecnt - 1];
-	  timecnt++;
-	}
-
 	rangeall.defaulttype = defaulttype;
-	rangeall.base = rangeall.leapbase = 0;
 	rangeall.count = timecnt;
 	rangeall.leapcount = leapcnt;
-	range64 = limitrange(rangeall, lo_time, hi_time, ats, types);
-	range32 = limitrange(range64, INT32_MIN, INT32_MAX, ats, types);
-
-	/*
-	** Remove old file, if any, to snap links.
-	*/
-	if (remove(name) == 0)
-		dir_checked = true;
-	else if (errno != ENOENT) {
-		const char *e = strerror(errno);
-
-		fprintf(stderr, _("%s: Can't remove %s/%s: %s\n"),
-			progname, directory, name, e);
-		exit(EXIT_FAILURE);
-	}
-	fp = fopen(name, "wb");
-	if (!fp) {
-	  int fopen_errno = errno;
-	  if (fopen_errno == ENOENT && !dir_checked) {
-	    mkdirs(name, true);
-	    fp = fopen(name, "wb");
-	    fopen_errno = errno;
+	range64 = limitrange(rangeall, lo_time,
+			     max(hi_time,
+				 redundant_time - (ZIC_MIN < redundant_time)),
+			     ats, types);
+	range32 = limitrange(range64, ZIC32_MIN, ZIC32_MAX, ats, types);
+
+	/* TZif version 4 is needed if a no-op transition is appended to
+	   indicate the expiration of the leap second table, or if the first
+	   leap second transition is not to a +1 or -1 correction.  */
+	for (pass = 1; pass <= 2; pass++) {
+	  struct timerange const *r = pass == 1 ? &range32 : &range64;
+	  if (pass == 1 && !want_bloat())
+	    continue;
+	  if (r->leapexpiry) {
+	    if (noise)
+	      warning(_("%s: pre-2021b clients may mishandle"
+			" leap second expiry"),
+		      name);
+	    version = '4';
 	  }
-	  if (!fp) {
-	    fprintf(stderr, _("%s: Can't create %s/%s: %s\n"),
-		    progname, directory, name, strerror(fopen_errno));
-	    exit(EXIT_FAILURE);
+	  if (0 < r->leapcount
+	      && corr[r->leapbase] != 1 && corr[r->leapbase] != -1) {
+	    if (noise)
+	      warning(_("%s: pre-2021b clients may mishandle"
+			" leap second table truncation"),
+		      name);
+	    version = '4';
 	  }
+	  if (version == '4')
+	    break;
 	}
+
+	fp = open_outfile(&outname, &tempname);
+
 	for (pass = 1; pass <= 2; ++pass) {
 		register ptrdiff_t thistimei, thistimecnt, thistimelim;
 		register int	thisleapi, thisleapcnt, thisleaplim;
-		int currenttype, thisdefaulttype;
-		bool locut, hicut;
-		zic_t lo;
+		struct tzhead tzh;
+		int pretranstype = -1, thisdefaulttype;
+		bool locut, hicut, thisleapexpiry;
+		zic_t lo, thismin, thismax;
 		int old0;
 		char		omittype[TZ_MAX_TYPES];
 		int		typemap[TZ_MAX_TYPES];
@@ -2023,27 +2513,15 @@  writezone(const char *const name, const char *const string, char version,
 		int		indmap[TZ_MAX_CHARS];
 
 		if (pass == 1) {
-			/* Arguably the default time type in the 32-bit data
-			   should be range32.defaulttype, which is suited for
-			   timestamps just before INT32_MIN.  However, zic
-			   traditionally used the time type of the indefinite
-			   past instead.  Internet RFC 8532 says readers should
-			   ignore 32-bit data, so this discrepancy matters only
-			   to obsolete readers where the traditional type might
-			   be more appropriate even if it's "wrong".  So, use
-			   the historical zic value, unless -r specifies a low
-			   cutoff that excludes some 32-bit timestamps.  */
-			thisdefaulttype = (lo_time <= INT32_MIN
-					   ? range64.defaulttype
-					   : range32.defaulttype);
-
+			thisdefaulttype = range32.defaulttype;
 			thistimei = range32.base;
 			thistimecnt = range32.count;
 			toomanytimes = thistimecnt >> 31 >> 1 != 0;
 			thisleapi = range32.leapbase;
 			thisleapcnt = range32.leapcount;
-			locut = INT32_MIN < lo_time;
-			hicut = hi_time < INT32_MAX;
+			thisleapexpiry = range32.leapexpiry;
+			thismin = ZIC32_MIN;
+			thismax = ZIC32_MAX;
 		} else {
 			thisdefaulttype = range64.defaulttype;
 			thistimei = range64.base;
@@ -2051,37 +2529,49 @@  writezone(const char *const name, const char *const string, char version,
 			toomanytimes = thistimecnt >> 31 >> 31 >> 2 != 0;
 			thisleapi = range64.leapbase;
 			thisleapcnt = range64.leapcount;
-			locut = min_time < lo_time;
-			hicut = hi_time < max_time;
+			thisleapexpiry = range64.leapexpiry;
+			thismin = min_time;
+			thismax = max_time;
 		}
 		if (toomanytimes)
 		  error(_("too many transition times"));
 
-		/* Keep the last too-low transition if no transition is
-		   exactly at LO.  The kept transition will be output as
-		   a LO "transition"; see "Output a LO_TIME transition"
-		   below.  This is needed when the output is truncated at
-		   the start, and is also useful when catering to buggy
-		   32-bit clients that do not use time type 0 for
+		locut = thismin < lo_time && lo_time <= thismax;
+		hicut = thismin <= hi_time && hi_time < thismax;
+		thistimelim = thistimei + thistimecnt;
+		memset(omittype, true, typecnt);
+
+		/* Determine whether to output a transition before the first
+		   transition in range.  This is needed when the output is
+		   truncated at the start, and is also useful when catering to
+		   buggy 32-bit clients that do not use time type 0 for
 		   timestamps before the first transition.  */
-		if (0 < thistimei && ats[thistimei] != lo_time) {
-		  thistimei--;
-		  thistimecnt++;
-		  locut = false;
+		if ((locut || (pass == 1 && thistimei))
+		    && ! (thistimecnt && ats[thistimei] == lo_time)) {
+		  pretranstype = thisdefaulttype;
+		  omittype[pretranstype] = false;
 		}
 
-		thistimelim = thistimei + thistimecnt;
-		thisleaplim = thisleapi + thisleapcnt;
-		if (thistimecnt != 0) {
-		  if (ats[thistimei] == lo_time)
-		    locut = false;
-		  if (hi_time < ZIC_MAX && ats[thistimelim - 1] == hi_time + 1)
-		    hicut = false;
-		}
-		memset(omittype, true, typecnt);
+		/* Arguably the default time type in the 32-bit data
+		   should be range32.defaulttype, which is suited for
+		   timestamps just before ZIC32_MIN.  However, zic
+		   traditionally used the time type of the indefinite
+		   past instead.  Internet RFC 8532 says readers should
+		   ignore 32-bit data, so this discrepancy matters only
+		   to obsolete readers where the traditional type might
+		   be more appropriate even if it's "wrong".  So, use
+		   the historical zic value, unless -r specifies a low
+		   cutoff that excludes some 32-bit timestamps.  */
+		if (pass == 1 && lo_time <= thismin)
+		  thisdefaulttype = range64.defaulttype;
+
+		if (locut)
+		  thisdefaulttype = unspecifiedtype;
 		omittype[thisdefaulttype] = false;
 		for (i = thistimei; i < thistimelim; i++)
 		  omittype[types[i]] = false;
+		if (hicut)
+		  omittype[unspecifiedtype] = false;
 
 		/* Reorder types to make THISDEFAULTTYPE type 0.
 		   Use TYPEMAP to swap OLD0 and THISDEFAULTTYPE so that
@@ -2102,7 +2592,13 @@  writezone(const char *const name, const char *const string, char version,
 			register int	mrudst, mrustd, hidst, histd, type;
 
 			hidst = histd = mrudst = mrustd = -1;
-			for (i = thistimei; i < thistimelim; ++i)
+			if (0 <= pretranstype) {
+			  if (isdsts[pretranstype])
+			    mrudst = pretranstype;
+			  else
+			    mrustd = pretranstype;
+			}
+			for (i = thistimei; i < thistimelim; i++)
 				if (isdsts[types[i]])
 					mrudst = types[i];
 				else	mrustd = types[i];
@@ -2172,19 +2668,20 @@  writezone(const char *const name, const char *const string, char version,
 			indmap[desigidx[i]] = j;
 		}
 		if (pass == 1 && !want_bloat()) {
-		  utcnt = stdcnt = thisleapcnt = 0;
-		  thistimecnt = - (locut + hicut);
+		  hicut = thisleapexpiry = false;
+		  pretranstype = -1;
+		  thistimecnt = thisleapcnt = 0;
 		  thistypecnt = thischarcnt = 1;
-		  thistimelim = thistimei;
 		}
 #define DO(field)	fwrite(tzh.field, sizeof tzh.field, 1, fp)
-		tzh = tzh0;
+		memset(&tzh, 0, sizeof tzh);
 		memcpy(tzh.tzh_magic, TZ_MAGIC, sizeof tzh.tzh_magic);
 		tzh.tzh_version[0] = version;
 		convert(utcnt, tzh.tzh_ttisutcnt);
 		convert(stdcnt, tzh.tzh_ttisstdcnt);
-		convert(thisleapcnt, tzh.tzh_leapcnt);
-		convert(locut + thistimecnt + hicut, tzh.tzh_timecnt);
+		convert(thisleapcnt + thisleapexpiry, tzh.tzh_leapcnt);
+		convert((0 <= pretranstype) + thistimecnt + hicut,
+			tzh.tzh_timecnt);
 		convert(thistypecnt, tzh.tzh_typecnt);
 		convert(thischarcnt, tzh.tzh_charcnt);
 		DO(tzh_magic);
@@ -2209,25 +2706,21 @@  writezone(const char *const name, const char *const string, char version,
 		/* Output a LO_TIME transition if needed; see limitrange.
 		   But do not go below the minimum representable value
 		   for this pass.  */
-		lo = pass == 1 && lo_time < INT32_MIN ? INT32_MIN : lo_time;
+		lo = pass == 1 && lo_time < ZIC32_MIN ? ZIC32_MIN : lo_time;
 
-		if (locut)
+		if (0 <= pretranstype)
 		  puttzcodepass(lo, fp, pass);
 		for (i = thistimei; i < thistimelim; ++i) {
-		  zic_t at = ats[i] < lo ? lo : ats[i];
-		  puttzcodepass(at, fp, pass);
+		  puttzcodepass(ats[i], fp, pass);
 		}
 		if (hicut)
 		  puttzcodepass(hi_time + 1, fp, pass);
-		currenttype = 0;
-		if (locut)
-		  putc(currenttype, fp);
-		for (i = thistimei; i < thistimelim; ++i) {
-		  currenttype = typemap[types[i]];
-		  putc(currenttype, fp);
-		}
+		if (0 <= pretranstype)
+		  putc(typemap[pretranstype], fp);
+		for (i = thistimei; i < thistimelim; i++)
+		  putc(typemap[types[i]], fp);
 		if (hicut)
-		  putc(currenttype, fp);
+		  putc(typemap[unspecifiedtype], fp);
 
 		for (i = old0; i < typecnt; i++) {
 		  int h = (i == old0 ? thisdefaulttype
@@ -2241,6 +2734,7 @@  writezone(const char *const name, const char *const string, char version,
 		if (thischarcnt != 0)
 			fwrite(thischars, sizeof thischars[0],
 				      thischarcnt, fp);
+		thisleaplim = thisleapi + thisleapcnt;
 		for (i = thisleapi; i < thisleaplim; ++i) {
 			register zic_t	todo;
 
@@ -2264,6 +2758,15 @@  writezone(const char *const name, const char *const string, char version,
 			puttzcodepass(todo, fp, pass);
 			puttzcode(corr[i], fp);
 		}
+		if (thisleapexpiry) {
+		  /* Append a no-op leap correction indicating when the leap
+		     second table expires.  Although this does not conform to
+		     Internet RFC 8536, most clients seem to accept this and
+		     the plan is to amend the RFC to allow this in version 4
+		     TZif files.  */
+		  puttzcodepass(leapexpires, fp, pass);
+		  puttzcode(thisleaplim ? corr[thisleaplim - 1] : 0, fp);
+		}
 		if (stdcnt != 0)
 		  for (i = old0; i < typecnt; i++)
 			if (!omittype[i])
@@ -2274,7 +2777,8 @@  writezone(const char *const name, const char *const string, char version,
 				putc(ttisuts[i], fp);
 	}
 	fprintf(fp, "\n%s\n", string);
-	close_file(fp, directory, name);
+	close_file(fp, directory, name, tempname);
+	rename_dest(tempname, name);
 	free(ats);
 }
 
@@ -2314,13 +2818,15 @@  abbroffset(char *buf, zic_t offset)
   }
 }
 
-static size_t
+static char const disable_percent_s[] = "";
+
+static ptrdiff_t
 doabbr(char *abbr, struct zone const *zp, char const *letters,
        bool isdst, zic_t save, bool doquotes)
 {
 	register char *	cp;
 	register char *	slashp;
-	register size_t	len;
+	ptrdiff_t len;
 	char const *format = zp->z_format;
 
 	slashp = strchr(format, '/');
@@ -2330,6 +2836,8 @@  doabbr(char *abbr, struct zone const *zp, char const *letters,
 	    letters = abbroffset(letterbuf, zp->z_stdoff + save);
 	  else if (!letters)
 	    letters = "%s";
+	  else if (letters == disable_percent_s)
+	    return 0;
 	  sprintf(abbr, format, letters);
 	} else if (isdst) {
 		strcpy(abbr, slashp + 1);
@@ -2467,11 +2975,17 @@  rule_cmp(struct rule const *a, struct rule const *b)
 		return 1;
 	if (a->r_hiyear != b->r_hiyear)
 		return a->r_hiyear < b->r_hiyear ? -1 : 1;
+	if (a->r_hiyear == ZIC_MAX)
+		return 0;
 	if (a->r_month - b->r_month != 0)
 		return a->r_month - b->r_month;
 	return a->r_dayofmonth - b->r_dayofmonth;
 }
 
+/* Store into RESULT a POSIX.1-2017 TZ string that represent the future
+   predictions for the zone ZPFIRST with ZONECOUNT entries.  Return a
+   compatibility indicator (a TZDB release year) if successful, a
+   negative integer if no such TZ string exissts.  */
 static int
 stringzone(char *result, struct zone const *zpfirst, ptrdiff_t zonecount)
 {
@@ -2480,12 +2994,16 @@  stringzone(char *result, struct zone const *zpfirst, ptrdiff_t zonecount)
 	register struct rule *		stdrp;
 	register struct rule *		dstrp;
 	register ptrdiff_t		i;
-	register const char *		abbrvar;
 	register int			compat = 0;
 	register int			c;
-	size_t				len;
 	int				offsetlen;
 	struct rule			stdr, dstr;
+	ptrdiff_t len;
+	int dstcmp;
+	struct rule *lastrp[2] = { NULL, NULL };
+	struct zone zstr[2];
+	struct zone const *stdzp;
+	struct zone const *dstzp;
 
 	result[0] = '\0';
 
@@ -2495,65 +3013,64 @@  stringzone(char *result, struct zone const *zpfirst, ptrdiff_t zonecount)
 	  return -1;
 
 	zp = zpfirst + zonecount - 1;
-	stdrp = dstrp = NULL;
 	for (i = 0; i < zp->z_nrules; ++i) {
+		struct rule **last;
+		int cmp;
 		rp = &zp->z_rules[i];
-		if (rp->r_hiwasnum || rp->r_hiyear != ZIC_MAX)
-			continue;
-		if (rp->r_yrtype != NULL)
-			continue;
-		if (!rp->r_isdst) {
-			if (stdrp == NULL)
-				stdrp = rp;
-			else	return -1;
-		} else {
-			if (dstrp == NULL)
-				dstrp = rp;
-			else	return -1;
-		}
-	}
-	if (stdrp == NULL && dstrp == NULL) {
-		/*
-		** There are no rules running through "max".
-		** Find the latest std rule in stdabbrrp
-		** and latest rule of any type in stdrp.
-		*/
-		register struct rule *stdabbrrp = NULL;
-		for (i = 0; i < zp->z_nrules; ++i) {
-			rp = &zp->z_rules[i];
-			if (!rp->r_isdst && rule_cmp(stdabbrrp, rp) < 0)
-				stdabbrrp = rp;
-			if (rule_cmp(stdrp, rp) < 0)
-				stdrp = rp;
-		}
-		if (stdrp != NULL && stdrp->r_isdst) {
-			/* Perpetual DST.  */
-			dstr.r_month = TM_JANUARY;
-			dstr.r_dycode = DC_DOM;
-			dstr.r_dayofmonth = 1;
-			dstr.r_tod = 0;
-			dstr.r_todisstd = dstr.r_todisut = false;
-			dstr.r_isdst = stdrp->r_isdst;
-			dstr.r_save = stdrp->r_save;
-			dstr.r_abbrvar = stdrp->r_abbrvar;
-			stdr.r_month = TM_DECEMBER;
-			stdr.r_dycode = DC_DOM;
-			stdr.r_dayofmonth = 31;
-			stdr.r_tod = SECSPERDAY + stdrp->r_save;
-			stdr.r_todisstd = stdr.r_todisut = false;
-			stdr.r_isdst = false;
-			stdr.r_save = 0;
-			stdr.r_abbrvar
-			  = (stdabbrrp ? stdabbrrp->r_abbrvar : "");
-			dstrp = &dstr;
-			stdrp = &stdr;
-		}
-	}
-	if (stdrp == NULL && (zp->z_nrules != 0 || zp->z_isdst))
-		return -1;
-	abbrvar = (stdrp == NULL) ? "" : stdrp->r_abbrvar;
-	len = doabbr(result, zp, abbrvar, false, 0, true);
-	offsetlen = stringoffset(result + len, - zp->z_stdoff);
+		last = &lastrp[rp->r_isdst];
+		cmp = rule_cmp(*last, rp);
+		if (cmp < 0)
+		  *last = rp;
+		else if (cmp == 0)
+		  return -1;
+	}
+	stdrp = lastrp[false];
+	dstrp = lastrp[true];
+	dstcmp = zp->z_nrules ? rule_cmp(dstrp, stdrp) : zp->z_isdst ? 1 : -1;
+	stdzp = dstzp = zp;
+
+	if (dstcmp < 0) {
+	  /* Standard time all year.  */
+	  dstrp = NULL;
+	} else if (0 < dstcmp) {
+	  /* DST all year.  Use an abbreviation like
+	     "XXX3EDT4,0/0,J365/23" for EDT (-04) all year.  */
+	  zic_t save = dstrp ? dstrp->r_save : zp->z_save;
+	  if (0 <= save)
+	    {
+	      /* Positive DST, the typical case for all-year DST.
+		 Fake a timezone with negative DST.  */
+	      stdzp = &zstr[0];
+	      dstzp = &zstr[1];
+	      zstr[0].z_stdoff = zp->z_stdoff + 2 * save;
+	      zstr[0].z_format = "XXX";  /* Any 3 letters will do.  */
+	      zstr[0].z_format_specifier = 0;
+	      zstr[1].z_stdoff = zstr[0].z_stdoff;
+	      zstr[1].z_format = zp->z_format;
+	      zstr[1].z_format_specifier = zp->z_format_specifier;
+	    }
+	  dstr.r_month = TM_JANUARY;
+	  dstr.r_dycode = DC_DOM;
+	  dstr.r_dayofmonth = 1;
+	  dstr.r_tod = 0;
+	  dstr.r_todisstd = dstr.r_todisut = false;
+	  dstr.r_isdst = true;
+	  dstr.r_save = save < 0 ? save : -save;
+	  dstr.r_abbrvar = dstrp ? dstrp->r_abbrvar : NULL;
+	  stdr.r_month = TM_DECEMBER;
+	  stdr.r_dycode = DC_DOM;
+	  stdr.r_dayofmonth = 31;
+	  stdr.r_tod = SECSPERDAY + dstr.r_save;
+	  stdr.r_todisstd = stdr.r_todisut = false;
+	  stdr.r_isdst = false;
+	  stdr.r_save = 0;
+	  stdr.r_abbrvar = save < 0 && stdrp ? stdrp->r_abbrvar : NULL;
+	  dstrp = &dstr;
+	  stdrp = &stdr;
+	}
+	len = doabbr(result, stdzp, stdrp ? stdrp->r_abbrvar : NULL,
+		     false, 0, true);
+	offsetlen = stringoffset(result + len, - stdzp->z_stdoff);
 	if (! offsetlen) {
 		result[0] = '\0';
 		return -1;
@@ -2561,11 +3078,11 @@  stringzone(char *result, struct zone const *zpfirst, ptrdiff_t zonecount)
 	len += offsetlen;
 	if (dstrp == NULL)
 		return compat;
-	len += doabbr(result + len, zp, dstrp->r_abbrvar,
+	len += doabbr(result + len, dstzp, dstrp->r_abbrvar,
 		      dstrp->r_isdst, dstrp->r_save, true);
 	if (dstrp->r_save != SECSPERMIN * MINSPERHOUR) {
 	  offsetlen = stringoffset(result + len,
-				   - (zp->z_stdoff + dstrp->r_save));
+				   - (dstzp->z_stdoff + dstrp->r_save));
 	  if (! offsetlen) {
 	    result[0] = '\0';
 	    return -1;
@@ -2573,7 +3090,7 @@  stringzone(char *result, struct zone const *zpfirst, ptrdiff_t zonecount)
 	  len += offsetlen;
 	}
 	result[len++] = ',';
-	c = stringrule(result + len, dstrp, dstrp->r_save, zp->z_stdoff);
+	c = stringrule(result + len, dstrp, dstrp->r_save, stdzp->z_stdoff);
 	if (c < 0) {
 		result[0] = '\0';
 		return -1;
@@ -2582,7 +3099,7 @@  stringzone(char *result, struct zone const *zpfirst, ptrdiff_t zonecount)
 		compat = c;
 	len += strlen(result + len);
 	result[len++] = ',';
-	c = stringrule(result + len, stdrp, dstrp->r_save, zp->z_stdoff);
+	c = stringrule(result + len, stdrp, dstrp->r_save, stdzp->z_stdoff);
 	if (c < 0) {
 		result[0] = '\0';
 		return -1;
@@ -2595,35 +3112,29 @@  stringzone(char *result, struct zone const *zpfirst, ptrdiff_t zonecount)
 static void
 outzone(const struct zone *zpfirst, ptrdiff_t zonecount)
 {
-	register const struct zone *	zp;
-	register struct rule *		rp;
 	register ptrdiff_t		i, j;
-	register bool			usestart, useuntil;
 	register zic_t			starttime, untiltime;
-	register zic_t			stdoff;
-	register zic_t			save;
-	register zic_t			year;
-	register zic_t			startoff;
 	register bool			startttisstd;
 	register bool			startttisut;
-	register int			type;
 	register char *			startbuf;
 	register char *			ab;
 	register char *			envvar;
 	register int			max_abbr_len;
 	register int			max_envvar_len;
-	register bool			prodstic; /* all rules are min to max */
 	register int			compat;
 	register bool			do_extend;
 	register char			version;
-	ptrdiff_t lastatmax = -1;
-	zic_t one = 1;
-	zic_t y2038_boundary = one << 31;
+	zic_t nonTZlimtime = ZIC_MIN;
+	int nonTZlimtype = -1;
 	zic_t max_year0;
 	int defaulttype = -1;
 
+	check_for_signal();
+
+	/* This cannot overflow; see FORMAT_LEN_GROWTH_BOUND.  */
 	max_abbr_len = 2 + max_format_len + max_abbrvar_len;
 	max_envvar_len = 2 * max_abbr_len + 5 * 9;
+
 	startbuf = emalloc(max_abbr_len + 1);
 	ab = emalloc(max_abbr_len + 1);
 	envvar = emalloc(max_envvar_len + 1);
@@ -2635,7 +3146,6 @@  outzone(const struct zone *zpfirst, ptrdiff_t zonecount)
 	timecnt = 0;
 	typecnt = 0;
 	charcnt = 0;
-	prodstic = zonecount == 1;
 	/*
 	** Thanks to Earl Chew
 	** for noting the need to unconditionally initialize startttisstd.
@@ -2648,29 +3158,27 @@  outzone(const struct zone *zpfirst, ptrdiff_t zonecount)
 		updateminmax(leapmaxyear + (leapmaxyear < ZIC_MAX));
 	}
 	for (i = 0; i < zonecount; ++i) {
-		zp = &zpfirst[i];
+		struct zone const *zp = &zpfirst[i];
 		if (i < zonecount - 1)
 			updateminmax(zp->z_untilrule.r_loyear);
 		for (j = 0; j < zp->z_nrules; ++j) {
-			rp = &zp->z_rules[j];
-			if (rp->r_lowasnum)
-				updateminmax(rp->r_loyear);
+			struct rule *rp = &zp->z_rules[j];
+			updateminmax(rp->r_loyear);
 			if (rp->r_hiwasnum)
 				updateminmax(rp->r_hiyear);
-			if (rp->r_lowasnum || rp->r_hiwasnum)
-				prodstic = false;
 		}
 	}
 	/*
 	** Generate lots of data if a rule can't cover all future times.
 	*/
 	compat = stringzone(envvar, zpfirst, zonecount);
-	version = compat < 2013 ? ZIC_VERSION_PRE_2013 : ZIC_VERSION;
+	version = compat < 2013 ? '2' : '3';
 	do_extend = compat < 0;
 	if (noise) {
 		if (!*envvar)
 			warning("%s %s",
-				_("no POSIX environment variable for zone"),
+				_("no POSIX.1-2017 environment variable"
+				  " for zone"),
 				zpfirst->z_name);
 		else if (compat != 0) {
 			/* Circa-COMPAT clients, and earlier clients, might
@@ -2682,64 +3190,44 @@  outzone(const struct zone *zpfirst, ptrdiff_t zonecount)
 		}
 	}
 	if (do_extend) {
-		/*
-		** Search through a couple of extra years past the obvious
-		** 400, to avoid edge cases.  For example, suppose a non-POSIX
-		** rule applies from 2012 onwards and has transitions in March
-		** and September, plus some one-off transitions in November
-		** 2013.  If zic looked only at the last 400 years, it would
-		** set max_year=2413, with the intent that the 400 years 2014
-		** through 2413 will be repeated.  The last transition listed
-		** in the tzfile would be in 2413-09, less than 400 years
-		** after the last one-off transition in 2013-11.  Two years
-		** might be overkill, but with the kind of edge cases
-		** available we're not sure that one year would suffice.
-		*/
-		enum { years_of_observations = YEARSPERREPEAT + 2 };
-
 		if (min_year >= ZIC_MIN + years_of_observations)
 			min_year -= years_of_observations;
 		else	min_year = ZIC_MIN;
 		if (max_year <= ZIC_MAX - years_of_observations)
 			max_year += years_of_observations;
 		else	max_year = ZIC_MAX;
-		/*
-		** Regardless of any of the above,
-		** for a "proDSTic" zone which specifies that its rules
-		** always have and always will be in effect,
-		** we only need one cycle to define the zone.
-		*/
-		if (prodstic) {
-			min_year = 1900;
-			max_year = min_year + years_of_observations;
-		}
 	}
+	max_year = max(max_year, (redundant_time / (SECSPERDAY * DAYSPERNYEAR)
+				  + EPOCH_YEAR + 1));
 	max_year0 = max_year;
 	if (want_bloat()) {
 	  /* For the benefit of older systems,
 	     generate data from 1900 through 2038.  */
-	  if (min_year > 1900)
-		min_year = 1900;
-	  if (max_year < 2038)
-		max_year = 2038;
+	  if (min_year > YEAR_32BIT_MIN - 1)
+		min_year = YEAR_32BIT_MIN - 1;
+	  if (max_year < YEAR_32BIT_MAX)
+		max_year = YEAR_32BIT_MAX;
 	}
 
+	if (min_time < lo_time || hi_time < max_time)
+	  unspecifiedtype = addtype(0, "-00", false, false, false);
+
 	for (i = 0; i < zonecount; ++i) {
-		struct rule *prevrp = NULL;
 		/*
 		** A guess that may well be corrected later.
 		*/
-		save = 0;
-		zp = &zpfirst[i];
-		usestart = i > 0 && (zp - 1)->z_untiltime > min_time;
-		useuntil = i < (zonecount - 1);
+		zic_t save = 0;
+		struct zone const *zp = &zpfirst[i];
+		bool usestart = i > 0 && (zp - 1)->z_untiltime > min_time;
+		bool useuntil = i < (zonecount - 1);
+		zic_t stdoff = zp->z_stdoff;
+		zic_t startoff = stdoff;
 		if (useuntil && zp->z_untiltime <= min_time)
 			continue;
-		stdoff = zp->z_stdoff;
-		eat(zp->z_filename, zp->z_linenum);
+		eat(zp->z_filenum, zp->z_linenum);
 		*startbuf = '\0';
-		startoff = zp->z_stdoff;
 		if (zp->z_nrules == 0) {
+			int type;
 			save = zp->z_save;
 			doabbr(startbuf, zp, NULL, zp->z_isdst, save, false);
 			type = addtype(oadd(zp->z_stdoff, save),
@@ -2747,23 +3235,31 @@  outzone(const struct zone *zpfirst, ptrdiff_t zonecount)
 				startttisut);
 			if (usestart) {
 				addtt(starttime, type);
+				if (useuntil && nonTZlimtime < starttime) {
+				  nonTZlimtime = starttime;
+				  nonTZlimtype = type;
+				}
 				usestart = false;
 			} else
 				defaulttype = type;
-		} else for (year = min_year; year <= max_year; ++year) {
+		} else {
+		  zic_t year;
+		  for (year = min_year; year <= max_year; ++year) {
 			if (useuntil && year > zp->z_untilrule.r_hiyear)
 				break;
 			/*
 			** Mark which rules to do in the current year.
 			** For those to do, calculate rpytime(rp, year);
+			** The former TYPE field was also considered here.
 			*/
 			for (j = 0; j < zp->z_nrules; ++j) {
-				rp = &zp->z_rules[j];
-				eats(zp->z_filename, zp->z_linenum,
-					rp->r_filename, rp->r_linenum);
+				zic_t one = 1;
+				zic_t y2038_boundary = one << 31;
+				struct rule *rp = &zp->z_rules[j];
+				eats(zp->z_filenum, zp->z_linenum,
+				     rp->r_filenum, rp->r_linenum);
 				rp->r_todo = year >= rp->r_loyear &&
-						year <= rp->r_hiyear &&
-						yearistype(year, rp->r_yrtype);
+						year <= rp->r_hiyear;
 				if (rp->r_todo) {
 					rp->r_temp = rpytime(rp, year);
 					rp->r_todo
@@ -2775,6 +3271,8 @@  outzone(const struct zone *zpfirst, ptrdiff_t zonecount)
 				register ptrdiff_t k;
 				register zic_t	jtime, ktime;
 				register zic_t	offset;
+				struct rule *rp;
+				int type;
 
 				INITIALIZE(ktime);
 				if (useuntil) {
@@ -2797,15 +3295,15 @@  outzone(const struct zone *zpfirst, ptrdiff_t zonecount)
 				*/
 				k = -1;
 				for (j = 0; j < zp->z_nrules; ++j) {
-					rp = &zp->z_rules[j];
-					if (!rp->r_todo)
+					struct rule *r = &zp->z_rules[j];
+					if (!r->r_todo)
 						continue;
-					eats(zp->z_filename, zp->z_linenum,
-						rp->r_filename, rp->r_linenum);
-					offset = rp->r_todisut ? 0 : stdoff;
-					if (!rp->r_todisstd)
+					eats(zp->z_filenum, zp->z_linenum,
+					     r->r_filenum, r->r_linenum);
+					offset = r->r_todisut ? 0 : stdoff;
+					if (!r->r_todisstd)
 						offset = oadd(offset, save);
-					jtime = rp->r_temp;
+					jtime = r->r_temp;
 					if (jtime == min_time ||
 						jtime == max_time)
 							continue;
@@ -2816,12 +3314,12 @@  outzone(const struct zone *zpfirst, ptrdiff_t zonecount)
 					} else if (jtime == ktime) {
 					  char const *dup_rules_msg =
 					    _("two rules for same instant");
-					  eats(zp->z_filename, zp->z_linenum,
-					       rp->r_filename, rp->r_linenum);
+					  eats(zp->z_filenum, zp->z_linenum,
+					       r->r_filenum, r->r_linenum);
 					  warning("%s", dup_rules_msg);
-					  rp = &zp->z_rules[k];
-					  eats(zp->z_filename, zp->z_linenum,
-					       rp->r_filename, rp->r_linenum);
+					  r = &zp->z_rules[k];
+					  eats(zp->z_filenum, zp->z_linenum,
+					       r->r_filenum, r->r_linenum);
 					  error("%s", dup_rules_msg);
 					}
 				}
@@ -2829,8 +3327,15 @@  outzone(const struct zone *zpfirst, ptrdiff_t zonecount)
 					break;	/* go on to next year */
 				rp = &zp->z_rules[k];
 				rp->r_todo = false;
-				if (useuntil && ktime >= untiltime)
+				if (useuntil && ktime >= untiltime) {
+					if (!*startbuf
+					    && (oadd(zp->z_stdoff, rp->r_save)
+						== startoff))
+					  doabbr(startbuf, zp, rp->r_abbrvar,
+						 rp->r_isdst, rp->r_save,
+						 false);
 					break;
+				}
 				save = rp->r_save;
 				if (usestart && ktime == starttime)
 					usestart = false;
@@ -2856,41 +3361,36 @@  outzone(const struct zone *zpfirst, ptrdiff_t zonecount)
 								false);
 					}
 				}
-				eats(zp->z_filename, zp->z_linenum,
-					rp->r_filename, rp->r_linenum);
+				eats(zp->z_filenum, zp->z_linenum,
+				     rp->r_filenum, rp->r_linenum);
 				doabbr(ab, zp, rp->r_abbrvar,
 				       rp->r_isdst, rp->r_save, false);
 				offset = oadd(zp->z_stdoff, rp->r_save);
-				if (!want_bloat() && !useuntil && !do_extend
-				    && prevrp
-				    && rp->r_hiyear == ZIC_MAX
-				    && prevrp->r_hiyear == ZIC_MAX)
-				  break;
 				type = addtype(offset, ab, rp->r_isdst,
 					rp->r_todisstd, rp->r_todisut);
 				if (defaulttype < 0 && !rp->r_isdst)
 				  defaulttype = type;
-				if (rp->r_hiyear == ZIC_MAX
-				    && ! (0 <= lastatmax
-					  && ktime < attypes[lastatmax].at))
-				  lastatmax = timecnt;
 				addtt(ktime, type);
-				prevrp = rp;
+				if (nonTZlimtime < ktime
+				    && (useuntil || rp->r_hiyear != ZIC_MAX)) {
+				  nonTZlimtime = ktime;
+				  nonTZlimtype = type;
+				}
 			}
+		  }
 		}
 		if (usestart) {
-			if (*startbuf == '\0' &&
-				zp->z_format != NULL &&
-				strchr(zp->z_format, '%') == NULL &&
-				strchr(zp->z_format, '/') == NULL)
-					strcpy(startbuf, zp->z_format);
-			eat(zp->z_filename, zp->z_linenum);
+			bool isdst = startoff != zp->z_stdoff;
+			if (*startbuf == '\0' && zp->z_format)
+			  doabbr(startbuf, zp, disable_percent_s,
+				 isdst, save, false);
+			eat(zp->z_filenum, zp->z_linenum);
 			if (*startbuf == '\0')
-error(_("can't determine time zone abbreviation to use just after until time"));
+			  error(_("can't determine time zone abbreviation"
+				  " to use just after until time"));
 			else {
-			  bool isdst = startoff != zp->z_stdoff;
-			  type = addtype(startoff, startbuf, isdst,
-					 startttisstd, startttisut);
+			  int type = addtype(startoff, startbuf, isdst,
+					     startttisstd, startttisut);
 			  if (defaulttype < 0 && !isdst)
 			    defaulttype = type;
 			  addtt(starttime, type);
@@ -2911,12 +3411,38 @@  error(_("can't determine time zone abbreviation to use just after until time"));
 	}
 	if (defaulttype < 0)
 	  defaulttype = 0;
-	if (0 <= lastatmax)
-	  attypes[lastatmax].dontmerge = true;
+	if (!do_extend && !want_bloat()) {
+	  /* Keep trailing transitions that are no greater than this.  */
+	  zic_t keep_at_max;
+
+	  /* The earliest transition into a time governed by the TZ string.  */
+	  zic_t TZstarttime = ZIC_MAX;
+	  for (i = 0; i < timecnt; i++) {
+	    zic_t at = attypes[i].at;
+	    if (nonTZlimtime < at && at < TZstarttime)
+	      TZstarttime = at;
+	  }
+	  if (TZstarttime == ZIC_MAX)
+	    TZstarttime = nonTZlimtime;
+
+	  /* Omit trailing transitions deducible from the TZ string,
+	     and not needed for -r or -R.  */
+	  keep_at_max = max(TZstarttime, redundant_time);
+	  for (i = j = 0; i < timecnt; i++)
+	    if (attypes[i].at <= keep_at_max) {
+	      attypes[j].at = attypes[i].at;
+	      attypes[j].dontmerge = (attypes[i].at == TZstarttime
+				      && (nonTZlimtype != attypes[i].type
+					  || strchr(envvar, ',')));
+	      attypes[j].type = attypes[i].type;
+	      j++;
+	    }
+	  timecnt = j;
+	}
 	if (do_extend) {
 		/*
-		** If we're extending the explicitly listed observations
-		** for 400 years because we can't fill the POSIX-TZ field,
+		** If we're extending the explicitly listed observations for
+		** 400 years because we can't fill the POSIX.1-2017 TZ field,
 		** check whether we actually ended up explicitly listing
 		** observations through that period.  If there aren't any
 		** near the end of the 400-year period, add a redundant
@@ -3005,6 +3531,10 @@  leapadd(zic_t t, int correction, int rolling)
 		error(_("too many leap seconds"));
 		exit(EXIT_FAILURE);
 	}
+	if (rolling && (lo_time != min_time || hi_time != max_time)) {
+	  error(_("Rolling leap seconds not supported with -r"));
+	  exit(EXIT_FAILURE);
+	}
 	for (i = 0; i < leapcnt; ++i)
 		if (t <= trans[i])
 			break;
@@ -3037,67 +3567,15 @@  adjleap(void)
 		last = corr[i] += last;
 	}
 
-	if (leapexpires < 0) {
-	  leapexpires = comment_leapexpires;
-	  if (0 <= leapexpires)
-	    warning(_("\"#expires\" is obsolescent; use \"Expires\""));
-	}
-
 	if (0 <= leapexpires) {
 	  leapexpires = oadd(leapexpires, last);
 	  if (! (leapcnt == 0 || (trans[leapcnt - 1] < leapexpires))) {
 	    error(_("last Leap time does not precede Expires time"));
 	    exit(EXIT_FAILURE);
 	  }
-	  if (leapexpires <= hi_time)
-	    hi_time = leapexpires - 1;
 	}
 }
 
-static char *
-shellquote(char *b, char const *s)
-{
-  *b++ = '\'';
-  while (*s) {
-    if (*s == '\'')
-      *b++ = '\'', *b++ = '\\', *b++ = '\'';
-    *b++ = *s++;
-  }
-  *b++ = '\'';
-  return b;
-}
-
-static bool
-yearistype(zic_t year, const char *type)
-{
-	char *buf;
-	char *b;
-	int result;
-
-	if (type == NULL || *type == '\0')
-		return true;
-	buf = emalloc(1 + 4 * strlen(yitcommand) + 2
-		      + INT_STRLEN_MAXIMUM(zic_t) + 2 + 4 * strlen(type) + 2);
-	b = shellquote(buf, yitcommand);
-	*b++ = ' ';
-	b += sprintf(b, "%"PRIdZIC, year);
-	*b++ = ' ';
-	b = shellquote(b, type);
-	*b = '\0';
-	result = system(buf);
-	if (WIFEXITED(result)) {
-	  int status = WEXITSTATUS(result);
-	  if (status <= 1) {
-	    free(buf);
-	    return status == 0;
-	  }
-	}
-	error(_("Wild result from command execution"));
-	fprintf(stderr, _("%s: command was '%s', result was %d\n"),
-		progname, buf, result);
-	exit(EXIT_FAILURE);
-}
-
 /* Is A a space character in the C locale?  */
 static bool
 is_space(char a)
@@ -3149,7 +3627,7 @@  lowerit(char a)
 }
 
 /* case-insensitive equality */
-static ATTRIBUTE_PURE bool
+ATTRIBUTE_REPRODUCIBLE static bool
 ciequal(register const char *ap, register const char *bp)
 {
 	while (lowerit(*ap) == lowerit(*bp++))
@@ -3158,7 +3636,7 @@  ciequal(register const char *ap, register const char *bp)
 	return false;
 }
 
-static ATTRIBUTE_PURE bool
+ATTRIBUTE_REPRODUCIBLE static bool
 itsabbr(register const char *abbr, register const char *word)
 {
 	if (lowerit(*abbr) != lowerit(*word))
@@ -3174,7 +3652,7 @@  itsabbr(register const char *abbr, register const char *word)
 
 /* Return true if ABBR is an initial prefix of WORD, ignoring ASCII case.  */
 
-static ATTRIBUTE_PURE bool
+ATTRIBUTE_REPRODUCIBLE static bool
 ciprefix(char const *abbr, char const *word)
 {
   do
@@ -3240,23 +3718,20 @@  byword(const char *word, const struct lookup *table)
 	return foundlp;
 }
 
-static char **
-getfields(register char *cp)
+static int
+getfields(char *cp, char **array, int arrayelts)
 {
 	register char *		dp;
-	register char **	array;
 	register int		nsubs;
 
-	if (cp == NULL)
-		return NULL;
-	array = emalloc(size_product(strlen(cp) + 1, sizeof *array));
 	nsubs = 0;
 	for ( ; ; ) {
+		char *dstart;
 		while (is_space(*cp))
 				++cp;
 		if (*cp == '\0' || *cp == '#')
 			break;
-		array[nsubs++] = dp = cp;
+		dstart = dp = cp;
 		do {
 			if ((*dp = *cp++) != '"')
 				++dp;
@@ -3271,43 +3746,50 @@  getfields(register char *cp)
 		if (is_space(*cp))
 			++cp;
 		*dp = '\0';
+		if (nsubs == arrayelts) {
+		  error(_("Too many input fields"));
+		  exit(EXIT_FAILURE);
+		}
+		array[nsubs++] = dstart + (*dstart == '-' && dp == dstart + 1);
 	}
-	array[nsubs] = NULL;
-	return array;
+	return nsubs;
 }
 
-static _Noreturn void
+ATTRIBUTE_NORETURN static void
 time_overflow(void)
 {
   error(_("time overflow"));
   exit(EXIT_FAILURE);
 }
 
-static ATTRIBUTE_PURE zic_t
+ATTRIBUTE_REPRODUCIBLE static zic_t
 oadd(zic_t t1, zic_t t2)
 {
-	if (t1 < 0 ? t2 < ZIC_MIN - t1 : ZIC_MAX - t1 < t2)
-	  time_overflow();
-	return t1 + t2;
+#ifdef ckd_add
+  zic_t sum;
+  if (!ckd_add(&sum, t1, t2))
+    return sum;
+#else
+  if (t1 < 0 ? ZIC_MIN - t1 <= t2 : t2 <= ZIC_MAX - t1)
+    return t1 + t2;
+#endif
+  time_overflow();
 }
 
-static ATTRIBUTE_PURE zic_t
+ATTRIBUTE_REPRODUCIBLE static zic_t
 tadd(zic_t t1, zic_t t2)
 {
-  if (t1 < 0) {
-    if (t2 < min_time - t1) {
-      if (t1 != min_time)
-	time_overflow();
-      return min_time;
-    }
-  } else {
-    if (max_time - t1 < t2) {
-      if (t1 != max_time)
-	time_overflow();
-      return max_time;
-    }
-  }
-  return t1 + t2;
+#ifdef ckd_add
+  zic_t sum;
+  if (!ckd_add(&sum, t1, t2) && min_time <= sum && sum <= max_time)
+    return sum;
+#else
+  if (t1 < 0 ? min_time - t1 <= t2 : t2 <= max_time - t1)
+    return t1 + t2;
+#endif
+  if (t1 == min_time || t1 == max_time)
+    return t1;
+  time_overflow();
 }
 
 /*
@@ -3321,32 +3803,28 @@  rpytime(const struct rule *rp, zic_t wantedy)
 	register int	m, i;
 	register zic_t	dayoff;			/* with a nod to Margaret O. */
 	register zic_t	t, y;
+	int yrem;
 
 	if (wantedy == ZIC_MIN)
 		return min_time;
 	if (wantedy == ZIC_MAX)
 		return max_time;
-	dayoff = 0;
 	m = TM_JANUARY;
 	y = EPOCH_YEAR;
-	if (y < wantedy) {
-	  wantedy -= y;
-	  dayoff = (wantedy / YEARSPERREPEAT) * (SECSPERREPEAT / SECSPERDAY);
-	  wantedy %= YEARSPERREPEAT;
-	  wantedy += y;
-	} else if (wantedy < 0) {
-	  dayoff = (wantedy / YEARSPERREPEAT) * (SECSPERREPEAT / SECSPERDAY);
-	  wantedy %= YEARSPERREPEAT;
-	}
+
+	/* dayoff = floor((wantedy - y) / YEARSPERREPEAT) * DAYSPERREPEAT,
+	   sans overflow.  */
+	yrem = wantedy % YEARSPERREPEAT - y % YEARSPERREPEAT;
+	dayoff = ((wantedy / YEARSPERREPEAT - y / YEARSPERREPEAT
+		   + yrem / YEARSPERREPEAT - (yrem % YEARSPERREPEAT < 0))
+		  * DAYSPERREPEAT);
+	/* wantedy = y + ((wantedy - y) mod YEARSPERREPEAT), sans overflow.  */
+	wantedy = y + (yrem + 2 * YEARSPERREPEAT) % YEARSPERREPEAT;
+
 	while (wantedy != y) {
-		if (wantedy > y) {
-			i = len_years[isleap(y)];
-			++y;
-		} else {
-			--y;
-			i = -len_years[isleap(y)];
-		}
+		i = len_years[isleap(y)];
 		dayoff = oadd(dayoff, i);
+		y++;
 	}
 	while (m != rp->r_month) {
 		i = len_months[isleap(y)][m];
@@ -3365,30 +3843,21 @@  rpytime(const struct rule *rp, zic_t wantedy)
 	--i;
 	dayoff = oadd(dayoff, i);
 	if (rp->r_dycode == DC_DOWGEQ || rp->r_dycode == DC_DOWLEQ) {
-		register zic_t	wday;
-
-#define LDAYSPERWEEK	((zic_t) DAYSPERWEEK)
-		wday = EPOCH_WDAY;
 		/*
 		** Don't trust mod of negative numbers.
 		*/
-		if (dayoff >= 0)
-			wday = (wday + dayoff) % LDAYSPERWEEK;
-		else {
-			wday -= ((-dayoff) % LDAYSPERWEEK);
-			if (wday < 0)
-				wday += LDAYSPERWEEK;
-		}
+		zic_t wday = ((EPOCH_WDAY + dayoff % DAYSPERWEEK + DAYSPERWEEK)
+			      % DAYSPERWEEK);
 		while (wday != rp->r_wday)
 			if (rp->r_dycode == DC_DOWGEQ) {
 				dayoff = oadd(dayoff, 1);
-				if (++wday >= LDAYSPERWEEK)
+				if (++wday >= DAYSPERWEEK)
 					wday = 0;
 				++i;
 			} else {
 				dayoff = oadd(dayoff, -1);
 				if (--wday < 0)
-					wday = LDAYSPERWEEK - 1;
+					wday = DAYSPERWEEK - 1;
 				--i;
 			}
 		if (i < 0 || i >= len_months[isleap(y)][m]) {
@@ -3440,14 +3909,12 @@  mp = _("time zone abbreviation differs from POSIX standard");
 /* Ensure that the directories of ARGNAME exist, by making any missing
    ones.  If ANCESTORS, do this only for ARGNAME's ancestors; otherwise,
    do it for ARGNAME too.  Exit with failure if there is trouble.
-   Do not consider an existing non-directory to be trouble.  */
+   Do not consider an existing file to be trouble.  */
 static void
 mkdirs(char const *argname, bool ancestors)
 {
-	register char *	name;
-	register char *	cp;
-
-	cp = name = ecpyalloc(argname);
+	char *name = estrdup(argname);
+	char *cp = name;
 
 	/* On MS-Windows systems, do not worry about drive letters or
 	   backslashes, as this should suffice in practice.  Time zone
@@ -3471,11 +3938,18 @@  mkdirs(char const *argname, bool ancestors)
 		** is checked anyway if the mkdir fails.
 		*/
 		if (mkdir(name, MKDIR_UMASK) != 0) {
-			/* For speed, skip itsdir if errno == EEXIST.  Since
-			   mkdirs is called only after open fails with ENOENT
-			   on a subfile, EEXIST implies itsdir here.  */
+			/* Do not report an error if err == EEXIST, because
+			   some other process might have made the directory
+			   in the meantime.  Likewise for ENOSYS, because
+			   Solaris 10 mkdir fails with ENOSYS if the
+			   directory is an automounted mount point.
+			   Likewise for EACCES, since mkdir can fail
+			   with EACCES merely because the parent directory
+			   is unwritable.  Likewise for most other error
+			   numbers.  */
 			int err = errno;
-			if (err != EEXIST && !itsdir(name)) {
+			if (err == ELOOP || err == ENAMETOOLONG
+			    || err == ENOENT || err == ENOTDIR) {
 				error(_("%s: Can't create directory %s: %s"),
 				      progname, name, strerror(err));
 				exit(EXIT_FAILURE);