git: 967a49a21a27 - main - Update tzcode to 2025b

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Wed, 27 Aug 2025 18:40:41 UTC
The branch main has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=967a49a21a27380ba1c545c746b4f1badabefd77

commit 967a49a21a27380ba1c545c746b4f1badabefd77
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-08-25 12:06:59 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2025-08-27 18:40:09 +0000

    Update tzcode to 2025b
    
    MFC after:      3 weeks
    
    Differential Revision:  https://reviews.freebsd.org/D52103
---
 contrib/tzcode/Makefile      |  32 +--
 contrib/tzcode/NEWS          | 109 +++++++-
 contrib/tzcode/asctime.c     | 122 +++++----
 contrib/tzcode/date.1        | 122 ++-------
 contrib/tzcode/localtime.c   | 585 +++++++++++++++++++++++++++----------------
 contrib/tzcode/newctime.3    | 152 ++++++-----
 contrib/tzcode/newstrftime.3 |  86 ++++---
 contrib/tzcode/newtzset.3    |  24 +-
 contrib/tzcode/private.h     | 119 ++++++---
 contrib/tzcode/strftime.c    |  83 ++++--
 contrib/tzcode/theory.html   |  40 ++-
 contrib/tzcode/tz-link.html  |  45 ++--
 contrib/tzcode/tzfile.5      |  70 +++---
 contrib/tzcode/tzfile.h      |   2 +-
 contrib/tzcode/tzselect.8    |  26 +-
 contrib/tzcode/version       |   2 +-
 contrib/tzcode/zdump.8       |  14 +-
 contrib/tzcode/zdump.c       |  62 ++---
 contrib/tzcode/zic.8         |  27 +-
 contrib/tzcode/zic.c         | 142 +++++++----
 20 files changed, 1117 insertions(+), 747 deletions(-)

diff --git a/contrib/tzcode/Makefile b/contrib/tzcode/Makefile
index 0087b4596515..2130582c2deb 100644
--- a/contrib/tzcode/Makefile
+++ b/contrib/tzcode/Makefile
@@ -137,7 +137,7 @@ TIME_T_ALTERNATIVES_TAIL = int_least32_t.ck uint_least32_t.ck \
   uint_least64_t.ck
 
 # What kind of TZif data files to generate.  (TZif is the binary time
-# zone data format that zic generates; see Internet RFC 8536.)
+# zone data format that zic generates; see Internet RFC 9636.)
 # If you want only POSIX time, with time values interpreted as
 # seconds since the epoch (not counting leap seconds), use
 #	REDO=		posix_only
@@ -255,6 +255,7 @@ LDLIBS=
 #  -DHAVE_UNISTD_H=0 if <unistd.h> does not work*
 #  -DHAVE_UTMPX_H=0 if <utmpx.h> does not work*
 #  -Dlocale_t=XXX if your system uses XXX instead of locale_t
+#  -DMKTIME_MIGHT_OVERFLOW if mktime might fail due to time_t overflow
 #  -DPORT_TO_C89 if tzcode should also run on mostly-C89 platforms+
 #	Typically it is better to use a later standard.  For example,
 #	with GCC 4.9.4 (2016), prefer '-std=gnu11' to '-DPORT_TO_C89'.
@@ -262,7 +263,7 @@ LDLIBS=
 #	feature (integers at least 64 bits wide) and maybe more.
 #  -DRESERVE_STD_EXT_IDS if your platform reserves standard identifiers
 #	with external linkage, e.g., applications cannot define 'localtime'.
-#  -Dssize_t=long on hosts like MS-Windows that lack ssize_t
+#  -Dssize_t=int on hosts like MS-Windows that lack ssize_t
 #  -DSUPPORT_C89=0 if the tzcode library should not support C89 callers
 #	Although -DSUPPORT_C89=0 might work around latent bugs in callers,
 #	it does not conform to POSIX.
@@ -285,7 +286,7 @@ LDLIBS=
 #	This mishandles some past timestamps, as US DST rules have changed.
 #	It also mishandles settings like TZ='EET-2EEST' for eastern Europe,
 #	as Europe and US DST rules differ.
-#  -DTZNAME_MAXIMUM=N to limit time zone abbreviations to N bytes (default 255)
+#  -DTZNAME_MAXIMUM=N to limit time zone abbreviations to N bytes (default 254)
 #  -DUNINIT_TRAP if reading uninitialized storage can cause problems
 #	other than simply getting garbage data
 #  -DUSE_LTZ=0 to build zdump with the system time zone library
@@ -319,7 +320,8 @@ GCC_DEBUG_FLAGS = -DGCC_LINT -g3 -O3 \
   $(GCC_INSTRUMENT) \
   -Wall -Wextra \
   -Walloc-size-larger-than=100000 -Warray-bounds=2 \
-  -Wbad-function-cast -Wbidi-chars=any,ucn -Wcast-align=strict -Wdate-time \
+  -Wbad-function-cast -Wbidi-chars=any,ucn -Wcast-align=strict -Wcast-qual \
+  -Wdate-time \
   -Wdeclaration-after-statement -Wdouble-promotion \
   -Wduplicated-branches -Wduplicated-cond -Wflex-array-member-not-at-end \
   -Wformat=2 -Wformat-overflow=2 -Wformat-signedness -Wformat-truncation \
@@ -336,7 +338,7 @@ GCC_DEBUG_FLAGS = -DGCC_LINT -g3 -O3 \
   -Wsuggest-attribute=noreturn -Wsuggest-attribute=pure \
   -Wtrampolines -Wundef -Wunused-macros -Wuse-after-free=3 \
   -Wvariadic-macros -Wvla -Wwrite-strings \
-  -Wno-format-nonliteral -Wno-sign-compare
+  -Wno-format-nonliteral -Wno-sign-compare -Wno-type-limits
 #
 # If your system has a "GMT offset" field in its "struct tm"s
 # (or if you decide to add such a field in your system's "time.h" file),
@@ -614,8 +616,8 @@ TZS_YEAR=	2050
 TZS_CUTOFF_FLAG=	-c $(TZS_YEAR)
 TZS=		to$(TZS_YEAR).tzs
 TZS_NEW=	to$(TZS_YEAR)new.tzs
-TZS_DEPS=	$(YDATA) asctime.c localtime.c \
-			private.h tzfile.h zdump.c zic.c
+TZS_DEPS=	$(YDATA) localtime.c private.h \
+			strftime.c tzfile.h zdump.c zic.c
 TZDATA_DIST = $(COMMON) $(DATA) $(MISC)
 # EIGHT_YARDS is just a yard short of the whole ENCHILADA.
 EIGHT_YARDS = $(TZDATA_DIST) $(DOCS) $(SOURCES) tzdata.zi
@@ -855,10 +857,10 @@ tzselect:	tzselect.ksh version
 		chmod +x $@.out
 		mv $@.out $@
 
-check: check_mild back.ck
+check: check_mild back.ck now.ck
 check_mild: check_web check_zishrink \
   character-set.ck white-space.ck links.ck mainguard.ck \
-  name-lengths.ck now.ck slashed-abbrs.ck sorted.ck \
+  name-lengths.ck slashed-abbrs.ck sorted.ck \
   tables.ck ziguard.ck tzs.ck
 
 # True if UTF8_LOCALE does not work;
@@ -1103,7 +1105,7 @@ set-timestamps.out: $(EIGHT_YARDS)
 		   touch -md @1 test.out; then \
 		  rm -f test.out && \
 		  for file in $$files; do \
-		    if git diff --quiet $$file; then \
+		    if git diff --quiet HEAD $$file; then \
 		      time=$$(TZ=UTC0 git log -1 \
 			--format='tformat:%cd' \
 			--date='format:%Y-%m-%dT%H:%M:%SZ' \
@@ -1354,13 +1356,13 @@ long-long.ck unsigned.ck: $(VERSION_DEPS)
 zonenames:	tzdata.zi
 		@$(AWK) '/^Z/ { print $$2 } /^L/ { print $$3 }' tzdata.zi
 
-asctime.o:	private.h tzfile.h
+asctime.o:	private.h
 date.o:		private.h
 difftime.o:	private.h
-localtime.o:	private.h tzfile.h tzdir.h
-strftime.o:	private.h tzfile.h
-zdump.o:	version.h
-zic.o:		private.h tzfile.h tzdir.h version.h
+localtime.o:	private.h tzdir.h tzfile.h
+strftime.o:	localtime.c private.h tzdir.h tzfile.h
+zdump.o:	private.h version.h
+zic.o:		private.h tzdir.h tzfile.h version.h
 
 .PHONY: ALL INSTALL all
 .PHONY: check check_mild check_time_t_alternatives
diff --git a/contrib/tzcode/NEWS b/contrib/tzcode/NEWS
index 83b8b8c8d39c..8c0771641ef0 100644
--- a/contrib/tzcode/NEWS
+++ b/contrib/tzcode/NEWS
@@ -1,5 +1,108 @@
 News for the tz database
 
+Release 2025b - 2025-03-22 13:40:46 -0700
+
+  Briefly:
+    New zone for Aysén Region in Chile which moves from -04/-03 to -03.
+
+  Changes to future timestamps
+
+    Chile's Aysén Region moves from -04/-03 to -03 year-round, joining
+    Magallanes Region.  The region will not change its clocks on
+    2025-04-05 at 24:00, diverging from America/Santiago and creating a
+    new zone America/Coyhaique.  (Thanks to Yonathan Dossow.)  Model
+    this as a change to standard offset effective 2025-03-20.
+
+  Changes to past timestamps
+
+    Iran switched from +04 to +0330 on 1978-11-10 at 24:00, not at
+    year end.  (Thanks to Roozbeh Pournader.)
+
+  Changes to code
+
+    'zic -l TIMEZONE -d . -l /some/other/file/system' no longer
+    attempts to create an incorrect symlink, and no longer has a
+    read buffer underflow.  (Problem reported by Evgeniy Gorbanev.)
+
+
+Release 2025a - 2025-01-15 10:47:24 -0800
+
+  Briefly:
+    Paraguay adopted permanent -03 starting spring 2024.
+    Improve pre-1991 data for the Philippines.
+    Etc/Unknown is now reserved.
+
+  Changes to future timestamps
+
+    Paraguay stopped changing its clocks after the spring-forward
+    transition on 2024-10-06, so it is now permanently at -03.
+    (Thanks to Heitor David Pinto and Even Scharning.)
+    This affects timestamps starting 2025-03-22, as well as the
+    obsolescent tm_isdst flags starting 2024-10-15.
+
+  Changes to past timestamps
+
+    Correct timestamps for the Philippines before 1900, and from 1937
+    through 1990.  (Thanks to P Chan for the heads-up and citations.)
+    This includes adjusting local mean time before 1899; fixing
+    transitions in September 1899, January 1937, and June 1954; adding
+    transitions in December 1941, November 1945, March and September
+    1977, and May and July 1990; and removing incorrect transitions in
+    March and September 1978.
+
+  Changes to data
+
+    Add zone1970.tab lines for the Concordia and Eyre Bird Observatory
+    research stations.  (Thanks to Derick Rethans and Jule Dabars.)
+
+  Changes to code
+
+    strftime %s now generates the correct numeric string even when the
+    represented number does not fit into time_t.  This is better than
+    generating the numeric equivalent of (time_t) -1, as strftime did
+    in TZDB releases 96a (when %s was introduced) through 2020a and in
+    releases 2022b through 2024b.  It is also better than failing and
+    returning 0, as strftime did in releases 2020b through 2022a.
+
+    strftime now outputs an invalid conversion specifier as-is,
+    instead of eliding the leading '%', which confused debugging.
+
+    An invalid TZ now generates the time zone abbreviation "-00", not
+    "UTC", to help the user see that an error has occurred.  (Thanks
+    to Arthur David Olson for suggesting a "wrong result".)
+
+    mktime and timeoff no longer incorrectly fail merely because a
+    struct tm component near INT_MIN or INT_MAX overflows when a
+    lower-order component carries into it.
+
+    TZNAME_MAXIMUM, the maximum number of bytes in a proleptic TZ
+    string's time zone abbreviation, now defaults to 254 not 255.
+    This helps reduce the size of internal state from 25480 to 21384
+    on common platforms.  This change should not be a problem, as
+    nobody uses such long "abbreviations" and the longstanding tzcode
+    maximum was 16 until release 2023a.  For those who prefer no
+    arbitrary limits, you can now specify TZNAME_MAXIMUM values up to
+    PTRDIFF_MAX, a limit forced by C anyway; formerly tzcode silently
+    misbehaved unless TZNAME_MAXIMUM was less than INT_MAX.
+
+    tzset and related functions no longer leak a file descriptor if
+    another thread forks or execs at about the same time and if the
+    platform has O_CLOFORK and O_CLOEXEC respectively.  Also, the
+    functions no longer let a TZif file become a controlling terminal.
+
+    'zdump -' now reads TZif data from /dev/stdin.
+    (From a question by Arthur David Olson.)
+
+  Changes to documentation
+
+    The name Etc/Unknown is now reserved: it will not be used by TZDB.
+    This is for compatibility with CLDR, which uses the string
+    "Etc/Unknown" for an unknown or invalid timezone.  (Thanks to
+    Justin Grant, Mark Davis, and Guy Harris.)
+
+    Cite Internet RFC 9636, which obsoletes RFC 8536 for TZif format.
+
+
 Release 2024b - 2024-09-04 12:27:47 -0700
 
   Briefly:
@@ -116,7 +219,7 @@ Release 2024b - 2024-09-04 12:27:47 -0700
   Changes to commentary
 
     Commentary about historical transitions in Portugal and her former
-    colonies has been expanded with links to many relevant legislation.
+    colonies has been expanded with links to relevant legislation.
     (Thanks to Tim Parenti.)
 
 
@@ -204,10 +307,10 @@ Release 2023d - 2023-12-21 20:02:24 -0800
     changing its time zone from -01/+00 to -02/-01 at the same moment
     as the spring-forward transition.  Its clocks will therefore not
     spring forward as previously scheduled.  The time zone change
-    reverts to its common practice before 1981.
+    reverts to its common practice before 1981.  (Thanks to Jule Dabars.)
 
     Fix predictions for DST transitions in Palestine in 2072-2075,
-    correcting a typo introduced in 2023a.
+    correcting a typo introduced in 2023a.  (Thanks to Jule Dabars.)
 
   Changes to past and future timestamps
 
diff --git a/contrib/tzcode/asctime.c b/contrib/tzcode/asctime.c
index ebb90a1cc84d..1977a2272896 100644
--- a/contrib/tzcode/asctime.c
+++ b/contrib/tzcode/asctime.c
@@ -7,6 +7,7 @@
 
 /*
 ** Avoid the temptation to punt entirely to strftime;
+** strftime can behave badly when tm components are out of range, and
 ** the output of strftime is supposed to be locale specific
 ** whereas the output of asctime is supposed to be constant.
 */
@@ -18,27 +19,6 @@
 #include "un-namespace.h"
 #include <stdio.h>
 
-/*
-** All years associated with 32-bit time_t values are exactly four digits long;
-** some years associated with 64-bit time_t values are not.
-** Vintage programs are coded for years that are always four digits long
-** and may assume that the newline always lands in the same place.
-** For years that are less than four digits, we pad the output with
-** leading zeroes to get the newline in the traditional place.
-** The -4 ensures that we get four characters of output even if
-** we call a strftime variant that produces fewer characters for some years.
-** This conforms to recent ISO C and POSIX standards, which say behavior
-** is undefined when the year is less than 1000 or greater than 9999.
-*/
-static char const ASCTIME_FMT[] = "%s %s%3d %.2d:%.2d:%.2d %-4s\n";
-/*
-** For years that are more than four digits we put extra spaces before the year
-** so that code trying to overwrite the newline won't end up overwriting
-** a digit within a year and truncating the year (operating on the assumption
-** that no output is better than wrong output).
-*/
-static char const ASCTIME_FMT_B[] = "%s %s%3d %.2d:%.2d:%.2d     %s\n";
-
 enum { STD_ASCTIME_BUF_SIZE = 26 };
 /*
 ** Big enough for something such as
@@ -52,14 +32,24 @@ enum { STD_ASCTIME_BUF_SIZE = 26 };
 */
 static char buf_asctime[2*3 + 5*INT_STRLEN_MAXIMUM(int) + 7 + 2 + 1 + 1];
 
-/* A similar buffer for ctime.
-   C89 requires that they be the same buffer.
-   This requirement was removed in C99, so support it only if requested,
-   as support is more likely to lead to bugs in badly written programs.  */
-#if SUPPORT_C89
-# define buf_ctime buf_asctime
-#else
-static char buf_ctime[sizeof buf_asctime];
+/* On pre-C99 platforms, a snprintf substitute good enough for us.  */
+#if !HAVE_SNPRINTF
+# include <stdarg.h>
+ATTRIBUTE_FORMAT((printf, 3, 4)) static int
+my_snprintf(char *s, size_t size, char const *format, ...)
+{
+  int n;
+  va_list args;
+  char stackbuf[sizeof buf_asctime];
+  va_start(args, format);
+  n = vsprintf(stackbuf, format, args);
+  va_end (args);
+  if (0 <= n && n < size)
+    memcpy (s, stackbuf, n + 1);
+  return n;
+}
+# undef snprintf
+# define snprintf my_snprintf
 #endif
 
 /* Publish asctime_r and ctime_r only when supporting older POSIX.  */
@@ -84,14 +74,19 @@ asctime_r(struct tm const *restrict timeptr, char *restrict buf)
 		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
 		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
 	};
-	const char *	wn;
-	const char *	mn;
-	char			year[INT_STRLEN_MAXIMUM(int) + 2];
-	char result[sizeof buf_asctime];
+	register const char *	wn;
+	register const char *	mn;
+	int year, mday, hour, min, sec;
+	long long_TM_YEAR_BASE = TM_YEAR_BASE;
+	size_t bufsize = (buf == buf_asctime
+			  ? sizeof buf_asctime : STD_ASCTIME_BUF_SIZE);
 
 	if (timeptr == NULL) {
+		strcpy(buf, "??? ??? ?? ??:??:?? ????\n");
+		/* Set errno now, since strcpy might change it in
+		   POSIX.1-2017 and earlier.  */
 		errno = EINVAL;
-		return strcpy(buf, "??? ??? ?? ??:??:?? ????\n");
+		return buf;
 	}
 	if (timeptr->tm_wday < 0 || timeptr->tm_wday >= DAYSPERWEEK)
 		wn = "???";
@@ -99,25 +94,41 @@ asctime_r(struct tm const *restrict timeptr, char *restrict buf)
 	if (timeptr->tm_mon < 0 || timeptr->tm_mon >= MONSPERYEAR)
 		mn = "???";
 	else	mn = mon_name[timeptr->tm_mon];
-	/*
-	** Use strftime's %Y to generate the year, to avoid overflow problems
-	** when computing timeptr->tm_year + TM_YEAR_BASE.
-	** Assume that strftime is unaffected by other out-of-range members
-	** (e.g., timeptr->tm_mday) when processing "%Y".
-	*/
-	strftime(year, sizeof year, "%Y", timeptr);
-	/*
-	** We avoid using snprintf since it's not available on all systems.
-	*/
-	sprintf(result,
-		((strlen(year) <= 4) ? ASCTIME_FMT : ASCTIME_FMT_B),
-		wn, mn,
-		timeptr->tm_mday, timeptr->tm_hour,
-		timeptr->tm_min, timeptr->tm_sec,
-		year);
-	if (strlen(result) < STD_ASCTIME_BUF_SIZE
-	    || buf == buf_ctime || buf == buf_asctime)
-		return strcpy(buf, result);
+
+	year = timeptr->tm_year;
+	mday = timeptr->tm_mday;
+	hour = timeptr->tm_hour;
+	min = timeptr->tm_min;
+	sec = timeptr->tm_sec;
+
+	/* Vintage programs are coded for years that are always four bytes long
+	   and may assume that the newline always lands in the same place.
+	   For years that are less than four bytes, pad the output with
+	   leading zeroes to get the newline in the traditional place.
+	   For years longer than four bytes, put extra spaces before the year
+	   so that vintage code trying to overwrite the newline
+	   won't overwrite a digit within a year and truncate the year,
+	   using the principle that no output is better than wrong output.
+	   This conforms to ISO C and POSIX standards, which say behavior
+	   is undefined when the year is less than 1000 or greater than 9999.
+
+	   Also, avoid overflow when formatting tm_year + TM_YEAR_BASE.  */
+
+	if ((year <= LONG_MAX - TM_YEAR_BASE
+	     ? snprintf (buf, bufsize,
+			 ((-999 - TM_YEAR_BASE <= year
+			   && year <= 9999 - TM_YEAR_BASE)
+			  ? "%s %s%3d %.2d:%.2d:%.2d %04ld\n"
+			  : "%s %s%3d %.2d:%.2d:%.2d     %ld\n"),
+			 wn, mn, mday, hour, min, sec,
+			 year + long_TM_YEAR_BASE)
+	     : snprintf (buf, bufsize,
+			 "%s %s%3d %.2d:%.2d:%.2d     %d%d\n",
+			 wn, mn, mday, hour, min, sec,
+			 year / 10 + TM_YEAR_BASE / 10,
+			 year % 10))
+	    < bufsize)
+		return buf;
 	else {
 		errno = EOVERFLOW;
 		return NULL;
@@ -142,5 +153,8 @@ ctime_r(const time_t *timep, char *buf)
 char *
 ctime(const time_t *timep)
 {
-  return ctime_r(timep, buf_ctime);
+  /* Do not call localtime_r, as C23 requires ctime to initialize the
+     static storage that localtime updates.  */
+  struct tm *tmp = localtime(timep);
+  return tmp ? asctime(tmp) : NULL;
 }
diff --git a/contrib/tzcode/date.1 b/contrib/tzcode/date.1
index 01907bc76e2c..3a02e7c2e08a 100644
--- a/contrib/tzcode/date.1
+++ b/contrib/tzcode/date.1
@@ -6,15 +6,13 @@ date \- show and set date and time
 .SH SYNOPSIS
 .if n .nh
 .if n .na
-.ie \n(.g .ds - \f(CR-\fP
-.el .ds - \-
 .B date
 [
-.B \*-u
+.B \-u
 ] [
-.B \*-c
+.B \-c
 ] [
-.B \*-r
+.B \-r
 .I seconds
 ] [
 .BI + format
@@ -35,7 +33,7 @@ command
 without arguments writes the date and time to the standard output in
 the form
 .ce 1
-Wed Mar  8 14:54:40 EST 1989
+Sat Mar  8 14:54:40 EST 2025
 .br
 with
 .B EST
@@ -49,99 +47,24 @@ If a command-line argument starts with a plus sign (\c
 .q "\fB+\fP" ),
 the rest of the argument is used as a
 .I format
-that controls what appears in the output.
-In the format, when a percent sign (\c
-.q "\fB%\fP"
-appears,
-it and the character after it are not output,
-but rather identify part of the date or time
-to be output in a particular way
-(or identify a special character to output):
-.nf
-.sp
-.if t .in +.5i
-.if n .in +2
-.ta \w'%M\0\0'u +\w'Wed Mar  8 14:54:40 EST 1989\0\0'u
-	Sample output	Explanation
-%a	Wed	Abbreviated weekday name*
-%A	Wednesday	Full weekday name*
-%b	Mar	Abbreviated month name*
-%B	March	Full month name*
-%c	Wed Mar 08 14:54:40 1989	Date and time*
-%C	19	Century
-%d	08	Day of month (always two digits)
-%D	03/08/89	Month/day/year (eight characters)
-%e	 8	Day of month (leading zero blanked)
-%h	Mar	Abbreviated month name*
-%H	14	24-hour-clock hour (two digits)
-%I	02	12-hour-clock hour (two digits)
-%j	067	Julian day number (three digits)
-%k	 2	12-hour-clock hour (leading zero blanked)
-%l	14	24-hour-clock hour (leading zero blanked)
-%m	03	Month number (two digits)
-%M	54	Minute (two digits)
-%n	\\n	newline character
-%p	PM	AM/PM designation
-%r	02:54:40 PM	Hour:minute:second AM/PM designation
-%R	14:54	Hour:minute
-%S	40	Second (two digits)
-%t	\\t	tab character
-%T	14:54:40	Hour:minute:second
-%U	10	Sunday-based week number (two digits)
-%w	3	Day number (one digit, Sunday is 0)
-%W	10	Monday-based week number (two digits)
-%x	03/08/89	Date*
-%X	14:54:40	Time*
-%y	89	Last two digits of year
-%Y	1989	Year in full
-%z	-0500	Numeric time zone
-%Z	EST	Time zone abbreviation
-%+	Wed Mar  8 14:54:40 EST 1989	Default output format*
-.if t .in -.5i
-.if n .in -2
-* The exact output depends on the locale.
-.sp
-.fi
-If a character other than one of those shown above appears after
-a percent sign in the format,
-that following character is output.
-All other characters in the format are copied unchanged to the output;
-a newline character is always added at the end of the output.
-.PP
-In Sunday-based week numbering,
-the first Sunday of the year begins week 1;
-days preceding it are part of
-.q "week 0" .
-In Monday-based week numbering,
-the first Monday of the year begins week 1.
-.PP
-To set the date, use a command line argument with one of the following forms:
-.nf
-.if t .in +.5i
-.if n .in +2
-.ta \w'198903081454\0'u
-1454	24-hour-clock hours (first two digits) and minutes
-081454	Month day (first two digits), hours, and minutes
-03081454	Month (two digits, January is 01), month day, hours, minutes
-8903081454	Year, month, month day, hours, minutes
-0308145489	Month, month day, hours, minutes, year
-	(on System V-compatible systems)
-030814541989	Month, month day, hours, minutes, four-digit year
-198903081454	Four-digit year, month, month day, hours, minutes
-.if t .in -.5i
-.if n .in -2
-.fi
-If the century, year, month, or month day is not given,
-the current value is used.
-Any of the above forms may be followed by a period and two digits that give
-the seconds part of the new time; if no seconds are given, zero is assumed.
+that is processed by
+.BR strftime (3)
+to determine what to output;
+a newline character is appended.
+For example, the shell command:
+.ce 1
+date +"%Y\-%m\-%d %H:%M:%S %z"
+.br
+outputs a line like
+.q "2025\-03\-08 14:54:40 \-0500"
+instead.
 .PP
 These options are available:
 .TP
-.BR \*-u " or " \*-c
+.BR \-u " or " \-c
 Use Universal Time when setting and showing the date and time.
 .TP
-.BI "\*-r " seconds
+.BI "\-r " seconds
 Output the date that corresponds to
 .I seconds
 past the epoch of 1970-01-01 00:00:00 UTC, where
@@ -149,16 +72,13 @@ past the epoch of 1970-01-01 00:00:00 UTC, where
 should be an integer, either decimal, octal (leading 0), or
 hexadecimal (leading 0x), preceded by an optional sign.
 .SH FILES
-.ta \w'/usr/share/zoneinfo/posixrules\0\0'u
+.ta \w'/usr/share/zoneinfo/Etc/UTC\0\0'u
 /etc/localtime	local timezone file
 .br
 /usr/lib/locale/\f2L\fP/LC_TIME	description of time locale \f2L\fP
 .br
 /usr/share/zoneinfo	timezone directory
 .br
-/usr/share/zoneinfo/posixrules	default DST rules (obsolete)
-.br
-/usr/share/zoneinfo/GMT	for UTC leap seconds
-.PP
-If /usr/share/zoneinfo/GMT is absent,
-UTC leap seconds are loaded from /usr/share/zoneinfo/GMT0 if present.
+/usr/share/zoneinfo/Etc/UTC	for UTC leap seconds
+.SH SEE ALSO
+.BR strftime (3).
diff --git a/contrib/tzcode/localtime.c b/contrib/tzcode/localtime.c
index 0fe7f1ed3f64..a80d422f2955 100644
--- a/contrib/tzcode/localtime.c
+++ b/contrib/tzcode/localtime.c
@@ -34,6 +34,14 @@ int __tz_change_interval = DETECT_TZ_CHANGES_INTERVAL;
 #include "un-namespace.h"
 #endif /* __FreeBSD__ */
 
+#if HAVE_SYS_STAT_H
+# include <sys/stat.h>
+#endif
+#if !defined S_ISREG && defined S_IFREG
+/* Ancient UNIX or recent MS-Windows.  */
+# define S_ISREG(mode) (((mode) & S_IFMT) == S_IFREG)
+#endif
+
 #if defined THREAD_SAFE && THREAD_SAFE
 # include <pthread.h>
 #ifdef __FreeBSD__
@@ -48,6 +56,73 @@ static int lock(void) { return 0; }
 static void unlock(void) { }
 #endif
 
+/* Unless intptr_t is missing, pacify gcc -Wcast-qual on char const * exprs.
+   Use this carefully, as the casts disable type checking.
+   This is a macro so that it can be used in static initializers.  */
+#ifdef INTPTR_MAX
+# define UNCONST(a) ((char *) (intptr_t) (a))
+#else
+# define UNCONST(a) ((char *) (a))
+#endif
+
+/* A signed type wider than int, so that we can add 1900 + tm_mon/12 to tm_year
+   without overflow.  The static_assert checks that it is indeed wider
+   than int; if this fails on your platform please let us know.  */
+#if INT_MAX < LONG_MAX
+typedef long iinntt;
+# define IINNTT_MIN LONG_MIN
+# define IINNTT_MAX LONG_MAX
+#elif INT_MAX < LLONG_MAX
+typedef long long iinntt;
+# define IINNTT_MIN LLONG_MIN
+# define IINNTT_MAX LLONG_MAX
+#else
+typedef intmax_t iinntt;
+# define IINNTT_MIN INTMAX_MIN
+# define IINNTT_MAX INTMAX_MAX
+#endif
+static_assert(IINNTT_MIN < INT_MIN && INT_MAX < IINNTT_MAX);
+
+/* On platforms where offtime or mktime might overflow,
+   strftime.c defines USE_TIMEX_T to be true and includes us.
+   This tells us to #define time_t to an internal type timex_t that is
+   wide enough so that strftime %s never suffers from integer overflow,
+   and to #define offtime (if TM_GMTOFF is defined) or mktime (otherwise)
+   to a static function that returns the redefined time_t.
+   It also tells us to define only data and code needed
+   to support the offtime or mktime variant.  */
+#ifndef USE_TIMEX_T
+# define USE_TIMEX_T false
+#endif
+#if USE_TIMEX_T
+# undef TIME_T_MIN
+# undef TIME_T_MAX
+# undef time_t
+# define time_t timex_t
+# if MKTIME_FITS_IN(LONG_MIN, LONG_MAX)
+typedef long timex_t;
+# define TIME_T_MIN LONG_MIN
+# define TIME_T_MAX LONG_MAX
+# elif MKTIME_FITS_IN(LLONG_MIN, LLONG_MAX)
+typedef long long timex_t;
+# define TIME_T_MIN LLONG_MIN
+# define TIME_T_MAX LLONG_MAX
+# else
+typedef intmax_t timex_t;
+# define TIME_T_MIN INTMAX_MIN
+# define TIME_T_MAX INTMAX_MAX
+# endif
+
+# ifdef TM_GMTOFF
+#  undef timeoff
+#  define timeoff timex_timeoff
+#  undef EXTERN_TIMEOFF
+# else
+#  undef mktime
+#  define mktime timex_mktime
+# endif
+#endif
+
 #ifndef TZ_ABBR_CHAR_SET
 # define TZ_ABBR_CHAR_SET \
 	"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 :+-._"
@@ -57,12 +132,23 @@ static void unlock(void) { }
 # define TZ_ABBR_ERR_CHAR '_'
 #endif /* !defined TZ_ABBR_ERR_CHAR */
 
-/*
-** Support non-POSIX platforms that distinguish between text and binary files.
-*/
+/* Port to platforms that lack some O_* flags.  Unless otherwise
+   specified, the flags are standardized by POSIX.  */
 
 #ifndef O_BINARY
-# define O_BINARY 0
+# define O_BINARY 0 /* MS-Windows */
+#endif
+#ifndef O_CLOEXEC
+# define O_CLOEXEC 0
+#endif
+#ifndef O_CLOFORK
+# define O_CLOFORK 0
+#endif
+#ifndef O_IGNORE_CTTY
+# define O_IGNORE_CTTY 0 /* GNU/Hurd */
+#endif
+#ifndef O_NOCTTY
+# define O_NOCTTY 0
 #endif
 
 #ifndef WILDABBR
@@ -91,7 +177,10 @@ static void unlock(void) { }
 static const char	wildabbr[] = WILDABBR;
 
 static char const etc_utc[] = "Etc/UTC";
+
+#if !USE_TIMEX_T || defined TM_ZONE || !defined TM_GMTOFF
 static char const *utc = etc_utc + sizeof "Etc/" - 1;
+#endif
 
 /*
 ** The DST rules to use if TZ has no rules and we can't load TZDEFRULES.
@@ -103,10 +192,31 @@ static char const *utc = etc_utc + sizeof "Etc/" - 1;
 # define TZDEFRULESTRING ",M3.2.0,M11.1.0"
 #endif
 
+/* Limit to time zone abbreviation length in proleptic TZ strings.
+   This is distinct from TZ_MAX_CHARS, which limits TZif file contents.
+   It defaults to 254, not 255, so that desigidx_type can be an unsigned char.
+   unsigned char suffices for TZif files, so the only reason to increase
+   TZNAME_MAXIMUM is to support TZ strings specifying abbreviations
+   longer than 254 bytes.  There is little reason to do that, though,
+   as strings that long are hardly "abbreviations".  */
+#ifndef TZNAME_MAXIMUM
+# define TZNAME_MAXIMUM 254
+#endif
+
+#if TZNAME_MAXIMUM < UCHAR_MAX
+typedef unsigned char desigidx_type;
+#elif TZNAME_MAXIMUM < INT_MAX
+typedef int desigidx_type;
+#elif TZNAME_MAXIMUM < PTRDIFF_MAX
+typedef ptrdiff_t desigidx_type;
+#else
+# error "TZNAME_MAXIMUM too large"
+#endif
+
 struct ttinfo {				/* time type information */
-	int_fast32_t	tt_utoff;	/* UT offset in seconds */
+	int_least32_t	tt_utoff;	/* UT offset in seconds */
+	desigidx_type	tt_desigidx;	/* abbreviation list index */
 	bool		tt_isdst;	/* used to set tm_isdst */
-	int		tt_desigidx;	/* abbreviation list index */
 	bool		tt_ttisstd;	/* transition is std time */
 	bool		tt_ttisut;	/* transition is UT */
 };
@@ -125,12 +235,6 @@ static char const UNSPEC[] = "-00";
    for ttunspecified to work without crashing.  */
 enum { CHARS_EXTRA = max(sizeof UNSPEC, 2) - 1 };
 
-/* Limit to time zone abbreviation length in proleptic TZ strings.
-   This is distinct from TZ_MAX_CHARS, which limits TZif file contents.  */
-#ifndef TZNAME_MAXIMUM
-# define TZNAME_MAXIMUM 255
-#endif
-
 /* A representation of the contents of a TZif file.  Ideally this
    would have no size limits; the following sizes should suffice for
    practical use.  This struct should not be too large, as instances
@@ -173,7 +277,6 @@ static struct tm *gmtsub(struct state const *, time_t const *, int_fast32_t,
 static bool increment_overflow(int *, int);
 static bool increment_overflow_time(time_t *, int_fast32_t);
 static int_fast32_t leapcorr(struct state const *, time_t);
-static bool normalize_overflow32(int_fast32_t *, int *, int);
 static struct tm *timesub(time_t const *, int_fast32_t, struct state const *,
 			  struct tm *);
 static bool tzparse(char const *, struct state *, struct state const *);
@@ -194,8 +297,10 @@ static struct state *const gmtptr = &gmtmem;
 # define TZ_STRLEN_MAX 255
 #endif /* !defined TZ_STRLEN_MAX */
 
+#if !USE_TIMEX_T || !defined TM_GMTOFF
 static char		lcl_TZname[TZ_STRLEN_MAX + 1];
 static int		lcl_is_set;
+#endif
 #ifdef __FreeBSD__
 static pthread_once_t	gmt_once = PTHREAD_ONCE_INIT;
 static pthread_once_t	gmtime_once = PTHREAD_ONCE_INIT;
@@ -221,27 +326,29 @@ static int		localtime_key_error;
 ** trigger latent bugs in programs.
 */
 
-#if SUPPORT_C89
+#if !USE_TIMEX_T
+
+# if SUPPORT_C89
 static struct tm	tm;
 #endif
 
-#if 2 <= HAVE_TZNAME + TZ_TIME_T
-char *			tzname[2] = {
-	(char *) wildabbr,
-	(char *) wildabbr
-};
-#endif
-#if 2 <= USG_COMPAT + TZ_TIME_T
+# if 2 <= HAVE_TZNAME + TZ_TIME_T
+char *tzname[2] = { UNCONST(wildabbr), UNCONST(wildabbr) };
+# endif
+# if 2 <= USG_COMPAT + TZ_TIME_T
 long			timezone;
 int			daylight;
-#endif
-#if 2 <= ALTZONE + TZ_TIME_T
+# endif
+# if 2 <= ALTZONE + TZ_TIME_T
 long			altzone;
+# endif
+
 #endif
 
 /* Initialize *S to a value based on UTOFF, ISDST, and DESIGIDX.  */
 static void
-init_ttinfo(struct ttinfo *s, int_fast32_t utoff, bool isdst, int desigidx)
+init_ttinfo(struct ttinfo *s, int_fast32_t utoff, bool isdst,
+	    desigidx_type desigidx)
 {
   s->tt_utoff = utoff;
   s->tt_isdst = isdst;
@@ -305,20 +412,22 @@ detzcode64(const char *const codep)
 	return result;
 }
 
+#if !USE_TIMEX_T || !defined TM_GMTOFF
+
 static void
 update_tzname_etc(struct state const *sp, struct ttinfo const *ttisp)
 {
-#if HAVE_TZNAME
-  tzname[ttisp->tt_isdst] = (char *) &sp->chars[ttisp->tt_desigidx];
-#endif
-#if USG_COMPAT
+# if HAVE_TZNAME
+  tzname[ttisp->tt_isdst] = UNCONST(&sp->chars[ttisp->tt_desigidx]);
+# endif
+# if USG_COMPAT
   if (!ttisp->tt_isdst)
     timezone = - ttisp->tt_utoff;
-#endif
-#if ALTZONE
+# endif
+# if ALTZONE
   if (ttisp->tt_isdst)
     altzone = - ttisp->tt_utoff;
-#endif
+# endif
 }
 
 /* If STDDST_MASK indicates that SP's TYPE provides useful info,
@@ -349,18 +458,18 @@ settzname(void)
 	   When STDDST_MASK becomes zero we can stop looking.  */
 	int stddst_mask = 0;
 
-#if HAVE_TZNAME
-	tzname[0] = tzname[1] = (char *) (sp ? wildabbr : utc);
+# if HAVE_TZNAME
+	tzname[0] = tzname[1] = UNCONST(sp ? wildabbr : utc);
 	stddst_mask = 3;
-#endif
-#if USG_COMPAT
+# endif
+# if USG_COMPAT
 	timezone = 0;
 	stddst_mask = 3;
-#endif
-#if ALTZONE
+# endif
+# if ALTZONE
 	altzone = 0;
 	stddst_mask |= 2;
-#endif
+# endif
 	/*
 	** And to get the latest time zone abbreviations into tzname. . .
 	*/
@@ -370,9 +479,9 @@ settzname(void)
 	  for (i = sp->typecnt - 1; stddst_mask && 0 <= i; i--)
 	    stddst_mask = may_update_tzname_etc(stddst_mask, sp, i);
 	}
-#if USG_COMPAT
+# if USG_COMPAT
 	daylight = stddst_mask >> 1 ^ 1;
-#endif
+# endif
 }
 
 /* Replace bogus characters in time zone abbreviations.
@@ -399,6 +508,8 @@ scrub_abbrs(struct state *sp)
 	return 0;
 }
 
+#endif
+
 #ifdef DETECT_TZ_CHANGES
 /*
  * Check whether either the time zone name or the file it refers to has
@@ -473,11 +584,15 @@ union local_storage {
 #endif /* !__FreeBSD__ */
 };
 
-/* Load tz data from the file named NAME into *SP.  Read extended
-   format if DOEXTEND.  Use *LSP for temporary storage.  Return 0 on
+/* These tzload flags can be ORed together, and fit into 'char'.  */
+enum { TZLOAD_FROMENV = 1 }; /* The TZ string came from the environment.  */
+enum { TZLOAD_TZSTRING = 2 }; /* Read any newline-surrounded TZ string.  */
+
+/* Load tz data from the file named NAME into *SP.  Respect TZLOADFLAGS.
+   Use *LSP for temporary storage.  Return 0 on
    success, an errno value on failure.  */
 static int
-tzloadbody(char const *name, struct state *sp, bool doextend,
+tzloadbody(char const *name, struct state *sp, char tzloadflags,
 	   union local_storage *lsp)
 {
 	register int			i;
@@ -485,7 +600,9 @@ tzloadbody(char const *name, struct state *sp, bool doextend,
 	register int			stored;
 	register ssize_t		nread;
 #ifdef __FreeBSD__
-	int serrno;
+	struct stat sb;
+	const char *relname;
+	int dd, serrno;
 #else /* !__FreeBSD__ */
 	register bool doaccess;
 #endif /* !__FreeBSD__ */
@@ -533,45 +650,75 @@ tzloadbody(char const *name, struct state *sp, bool doextend,
 
 		name = lsp->fullname;
 	}
-	if (doaccess && access(name, R_OK) != 0)
-	  return errno;
-	fid = _open(name, O_RDONLY | O_BINARY);
+	if (doaccess && (tzloadflags & TZLOAD_FROMENV)) {
+	  /* Check for security violations and for devices whose mere
+	     opening could have unwanted side effects.  Although these
+	     checks are racy, they're better than nothing and there is
+	     no portable way to fix the races.  */
+	  if (access(name, R_OK) < 0)
+	    return errno;
+#ifdef S_ISREG
+	  {
+	    struct stat st;
+	    if (stat(name, &st) < 0)
+	      return errno;
+	    if (!S_ISREG(st.st_mode))
+	      return EINVAL;
+	  }
+#endif
*** 2836 LINES SKIPPED ***