diff mbox series

[3/4] time: Do not perform forced DST adjustment for DST-less zones

Message ID 435ebea3d916f61759f8f3f3424365e2ccd5e810.1727784647.git.fweimer@redhat.com
State New
Headers show
Series mktime tm_isdst compatibility improvements | expand

Commit Message

Florian Weimer Oct. 1, 2024, 12:14 p.m. UTC
For zones such as UTC, it does not really make sense to perform
the forced adjustment.

The timezone/testdata/IST file is the compiled Asia/Kolkata zone
from tz 2024a.
---
 manual/time.texi             |  41 ++++++++-
 time/Makefile                |   1 +
 time/mktime.c                |   8 +-
 time/tst-mktime-dst-adjust.c | 156 +++++++++++++++++++++++++++++++++++
 timezone/testdata/IST        | Bin 0 -> 285 bytes
 5 files changed, 202 insertions(+), 4 deletions(-)
 create mode 100644 time/tst-mktime-dst-adjust.c
 create mode 100644 timezone/testdata/IST

GIT binary patch
literal 285
zcmWHE%1kq2zzf(I7#LU>7#M^a7#JAZ=kD2c>UNLD8P-CHGgFOLTq+To!N|nS#LUFN
z5Of1%j<*{~wQmFi2LnTN1|yG;ZwQ00ZwP~Da0r98ftj%ZLkM9z*%=rZSQ!`?#Qy*P
z|6gsFga(Lqe*W43M1$M_qCxHe(IB^g>8VC5Ks3}%EX*v-Fh>Pl06EIr4df`_2nHUo
W(*!_H^T2Q(muGN@uBnx=0T%#v+DeN6

literal 0
HcmV?d00001
diff mbox series

Patch

diff --git a/manual/time.texi b/manual/time.texi
index 64aad8fdc5..c72febc7c6 100644
--- a/manual/time.texi
+++ b/manual/time.texi
@@ -1075,9 +1075,16 @@  This is a flag that indicates whether daylight saving time is (or was, or
 will be) in effect at the time described.  The value is positive if
 daylight saving time is in effect, zero if it is not, and negative if the
 information is not available.
-Although this flag is useful when passing a broken-down time to the
-@code{mktime} function, for other uses this flag should be ignored and
-the @code{tm_gmtoff} and @code{tm_zone} fields should be inspected instead.
+
+It is recommended to set this field to a negative value when calling
+@code{mktime} unless the application has access to the correct value of
+the daylight saving time status at the specified time.  This instructs
+@code{mktime} to use time zone data to determine whether or not to apply
+daylight saving time.
+
+Reading the @code{tm_isdst} field after calling @code{localtime}
+and similar functions is not recommended.  Instead inspect the
+the @code{tm_gmtoff} and @code{tm_zone} fields.
 
 @item long int tm_gmtoff
 This field describes the time zone that was used to compute this
@@ -1292,6 +1299,33 @@  simple time representation.  It also normalizes the contents of the
 broken-down time structure, and fills in some components based on the
 values of the others.
 
+In general, it is recommended to set the @code{tm_isdst} member of
+@var{brokentime} to a negative value prior to calling this function.
+However, if @code{*@var{brokentime}} was produced by @code{localtime} or
+similar functions, a correct value of @code{tm_isdst} allows to
+disambiguate broken-down time representations during the overlapping
+window after time jumped back during a daylight saving time transition.
+This is a scenario where @code{tm_isdst} can be helpful, and the
+adjustment described below does not produce unexpected @code{mktime}
+results.
+
+If @code{tm_isdst} is negative, @code{mktime} uses time zone data to
+determine whether daylight saving time is in effect at the requested
+time.
+
+In the other case, @code{tm_isdst} is not negative: the value zero
+indicates that no daylight saving time is in effect, and positive value
+means it is in effect.  If time zone data and the @code{tm_isdst} flag
+disagree, @code{mktime} will forcefully adjust the returned value, using
+an unspecified heuristic to obtain the daylight saving time offset.
+Typically, this means that @code{mktime} adds one hour if
+@code{tm_isdst} is zero, or subtracts one hour if @code{tm_isdst} is
+positive.  This forced adjustment only happens if time zone data
+indicates that there is at least one daylight saving transition (at any
+time).  For zones such as @samp{UTC} which never use daylight saving
+time, the adjustment does not happen even if @code{tm_isdst} is
+positive.
+
 The @code{mktime} function ignores the specified contents of the
 @code{tm_wday}, @code{tm_yday}, @code{tm_gmtoff}, and @code{tm_zone}
 members of the broken-down time
@@ -1342,6 +1376,7 @@  available.  @code{timelocal} is rather rare.
 @c     tzfile_compute(!use_localtime) ok
 
 @code{timegm} is functionally identical to @code{mktime} except it
+sets @code{@var{brokentime}->tm_isdst} to zero and
 always takes the input values to be UTC
 regardless of any local time zone setting.
 
diff --git a/time/Makefile b/time/Makefile
index d06797b06c..0cfc4c9d51 100644
--- a/time/Makefile
+++ b/time/Makefile
@@ -64,6 +64,7 @@  tests := \
   tst-gmtime \
   tst-itimer \
   tst-mktime \
+  tst-mktime-dst-adjust \
   tst-mktime2 \
   tst-mktime3 \
   tst-mktime4 \
diff --git a/time/mktime.c b/time/mktime.c
index 669bc46c34..ad371ba57f 100644
--- a/time/mktime.c
+++ b/time/mktime.c
@@ -433,7 +433,13 @@  __mktime_internal (struct tm *tp, bool local, mktime_offset_t *offset)
 
   /* We have a match.  Check whether tm.tm_isdst has the requested
      value, if any.  */
-  if (isdst_differ (isdst, tm.tm_isdst))
+  if (isdst_differ (isdst, tm.tm_isdst)
+#if _LIBC
+      /* Only perform the adjustment if the time zone ever uses DST.
+         This skips the adjustment for UTC, for example.  */
+      && __daylight
+#endif
+      )
     {
       /* tm.tm_isdst has the wrong value.  Look for a neighboring
 	 time with the right value, and use its UTC offset.
diff --git a/time/tst-mktime-dst-adjust.c b/time/tst-mktime-dst-adjust.c
new file mode 100644
index 0000000000..416ef2abcd
--- /dev/null
+++ b/time/tst-mktime-dst-adjust.c
@@ -0,0 +1,156 @@ 
+/* Test mktime DST adjustment special cases.
+   Copyright (C) 2024 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <time.h>
+#include <stdlib.h>
+#include <support/check.h>
+
+static int
+do_test (void)
+{
+  TEST_COMPARE (setenv ("TZ", "UTC", 1), 0);
+
+  {
+    struct tm t =
+      {
+        .tm_year = 124,
+        .tm_mon = 9,
+        .tm_mday = 1,
+        .tm_hour = 9,
+        .tm_min = 20,
+        .tm_sec = 53,
+        .tm_isdst = 1,            /* Not actually true.  */
+      };
+    TEST_COMPARE (mktime (&t), 1727774453);
+  }
+
+  /* IST used DST at one point, but no longer does.  */
+  {
+    char *path = realpath ("../timezone/testdata/IST", NULL);
+    TEST_VERIFY (path != NULL);
+    TEST_COMPARE (setenv ("TZ", path, 1), 0);
+    free (path);
+  }
+
+  {
+    struct tm t =
+      {
+        .tm_year = 124,
+        .tm_mon = 9,
+        .tm_mday = 1,
+        .tm_hour = 9,
+        .tm_min = 20,
+        .tm_sec = 53,
+        .tm_isdst = 0,          /* Correct value.  */
+      };
+    TEST_COMPARE (mktime (&t), 1727774453 - (int) (5.5 * 3600));
+    TEST_COMPARE (t.tm_gmtoff, (int) (5.5 * 3600));
+    TEST_COMPARE (t.tm_isdst, 0);
+  }
+
+  /* This value is incorrect, but the heuristic ignores historic
+     DST changes.  */
+  {
+    struct tm t =
+      {
+        .tm_year = 124,
+        .tm_mon = 9,
+        .tm_mday = 1,
+        .tm_hour = 9,
+        .tm_min = 20,
+        .tm_sec = 53,
+        .tm_isdst = 1,          /* Incorrect value.  */
+      };
+    TEST_COMPARE (mktime (&t), 1727774453 - (int) (5.5 * 3600));
+    TEST_COMPARE (t.tm_gmtoff, (int) (5.5 * 3600));
+    TEST_COMPARE (t.tm_isdst, 0);
+  }
+
+  /* Test using correct DST.  */
+  {
+    struct tm t =
+      {
+        .tm_year = 42,
+        .tm_mon = 9,
+        .tm_mday = 1,
+        .tm_hour = 9,
+        .tm_min = 20,
+        .tm_sec = 53,
+        .tm_isdst = 1,          /* Correct value, DST was in effect.  */
+      };
+    TEST_COMPARE (mktime (&t), -860015347);
+    TEST_COMPARE (t.tm_gmtoff, (int) (6.5 * 3600));
+    TEST_COMPARE (t.tm_isdst, 1);
+  }
+
+  /* Mismatch: DST incorrectly claimed not in effect.  */
+
+  {
+    struct tm t =
+      {
+        .tm_year = 42,
+        .tm_mon = 9,
+        .tm_mday = 1,
+        .tm_hour = 9,
+        .tm_min = 20,
+        .tm_sec = 53,
+        .tm_isdst = 0,          /* Incorrect value.  */
+      };
+    TEST_COMPARE (mktime (&t), -860015347 + 3600); /* One hour added.  */
+    TEST_COMPARE (t.tm_gmtoff, (int) (6.5 * 3600));
+    TEST_COMPARE (t.tm_isdst, 1);
+  }
+
+  /* Test using correct standard time.  */
+  {
+    struct tm t =
+      {
+        .tm_year = 42,
+        .tm_mon = 7,
+        .tm_mday = 1,
+        .tm_hour = 9,
+        .tm_min = 20,
+        .tm_sec = 53,
+        .tm_isdst = 0,          /* Correct value, standard time in effect.  */
+      };
+    TEST_COMPARE (mktime (&t), -865282147);
+    TEST_COMPARE (t.tm_gmtoff, (int) (5.5 * 3600));
+    TEST_COMPARE (t.tm_isdst, 0);
+  }
+
+  /* Test using standard time with mismatch.  */
+  {
+    struct tm t =
+      {
+        .tm_year = 42,
+        .tm_mon = 7,
+        .tm_mday = 1,
+        .tm_hour = 9,
+        .tm_min = 20,
+        .tm_sec = 53,
+        .tm_isdst = 1,          /* Incorrect value.  */
+      };
+    TEST_COMPARE (mktime (&t), -865282147 - 3600); /* One hour subtracted.  */
+    TEST_COMPARE (t.tm_gmtoff, (int) (5.5 * 3600));
+    TEST_COMPARE (t.tm_isdst, 0);
+  }
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/timezone/testdata/IST b/timezone/testdata/IST
new file mode 100644
index 0000000000000000000000000000000000000000..0014046d29a38e9b8006f746fea794d7f71eb479