[PATCH v5] ctime.3: Document how to detect invalid or ambiguous times

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



This example documents the corner cases of mktime(3), such as what
happens during DST transitions, and other jumps in the calendar.

Link: <https://www.redhat.com/en/blog/brief-history-mktime>
Reported-by: DJ Delorie <dj@xxxxxxxxxx>
Cc: Carlos O'Donell <carlos@xxxxxxxxxx>
Cc: Paul Eggert <eggert@xxxxxxxxxxx>
Signed-off-by: Alejandro Colomar <alx@xxxxxxxxxx>
---

Hi DJ, Paul,

This is just a resend of v4.  Forget v4.

I forgot to send it in reply to the previous versions, and forgot to
paste the rendered output.

Here's how this looks:

CAVEATS
     ...

   mktime()
     ...

     Passing an invalid time to mktime()  or  an  invalid  tm‐>tm_isdst
     value yields an unspecified result.  Also, passing the value -1 in
     tm‐>tm_isdst  will  result  in  an  ambiguous time during some DST
     transitions, which will also yield an unspecified result.  See the
     example program in EXAMPLES.

EXAMPLES
     The program below defines a wrapper that allows detecting  invalid
     and ambiguous times, with EINVAL and ENOTUNIQ, respectively.

     The following shell session shows sample runs of the program:

         $ TZ=UTC ./a.out 1969 12 31 23 59 59 0;
         -1
         $
         $ export TZ=Europe/Madrid;
         $
         $ ./a.out 2147483647 2147483647 00 00 00 00 ‐1;
         a.out: mktime: Value too large for defined data type
         $
         $ ./a.out 2024 08 23 00 17 53 -1;
         1724365073
         $ ./a.out 2024 08 23 00 17 53 0;
         a.out: mktime: Invalid argument
         1724368673
         $ ./a.out 2024 08 23 00 17 53 1;
         1724365073
         $
         $ ./a.out 2024 02 23 00 17 53 -1;
         1708643873
         $ ./a.out 2024 02 23 00 17 53 0;
         1708643873
         $ ./a.out 2024 02 23 00 17 53 1;
         a.out: mktime: Invalid argument
         1708640273
         $
         $ ./a.out 2023 03 26 02 17 53 -1;
         a.out: mktime: Invalid argument
         1679793473
         $
         $ ./a.out 2023 10 29 02 17 53 -1;
         a.out: mktime: Name not unique on network
         1698542273
         $ ./a.out 2023 10 29 02 17 53 0;
         1698542273
         $ ./a.out 2023 10 29 02 17 53 1;
         1698538673
         $
         $ ./a.out 2023 02 29 12 00 00 -1;
         a.out: mktime: Invalid argument
         1677668400

   Program source: mktime.c

     #include <err.h>
     #include <errno.h>
     #include <stdint.h>
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     #include <time.h>

     #define is_signed(T)  ((T) -1 < 1)

     time_t my_mktime(struct tm *tp);

     int
     main(int argc, char *argv[])
     {
         char       **p;
         time_t     t;
         struct tm  tm;

         if (argc != 8) {
             fprintf(stderr, "Usage: %s yyyy mm dd HH MM SS isdst\n", argv[0]);
             exit(EXIT_FAILURE);
         }

         p = &argv[1];
         tm.tm_year  = atoi(*p++) - 1900;
         tm.tm_mon   = atoi(*p++) - 1;
         tm.tm_mday  = atoi(*p++);
         tm.tm_hour  = atoi(*p++);
         tm.tm_min   = atoi(*p++);
         tm.tm_sec   = atoi(*p++);
         tm.tm_isdst = atoi(*p++);

         errno = 0;
         tm.tm_wday = -1;
         t = my_mktime(&tm);
         if (tm.tm_wday == -1)
             err(EXIT_FAILURE, "mktime");
         if (errno == EINVAL || errno == ENOTUNIQ)
             warn("mktime");

         if (is_signed(time_t))
             printf("%jd\n", (intmax_t) t);
         else
             printf("%ju\n", (uintmax_t) t);

         exit(EXIT_SUCCESS);
     }

     time_t
     my_mktime(struct tm *tp)
     {
         int            e, isdst;
         time_t         t;
         struct tm      tm;
         unsigned char  wday[sizeof(tp->tm_wday)];

         e = errno;

         tm = *tp;
         isdst = tp->tm_isdst;

         memcpy(wday, &tp->tm_wday, sizeof(wday));
         tp->tm_wday = -1;
         t = mktime(tp);
         if (tp->tm_wday == -1) {
             memcpy(&tp->tm_wday, wday, sizeof(wday));
             return -1;
         }

         if (isdst == -1)
             tm.tm_isdst = tp->tm_isdst;

         if (   tm.tm_sec   != tp->tm_sec
             || tm.tm_min   != tp->tm_min
             || tm.tm_hour  != tp->tm_hour
             || tm.tm_mday  != tp->tm_mday
             || tm.tm_mon   != tp->tm_mon
             || tm.tm_year  != tp->tm_year
             || tm.tm_isdst != tp->tm_isdst)
         {
             errno = EINVAL;
             return t;
         }

         if (isdst != -1)
             goto out;

         tm = *tp;
         tm.tm_isdst = !tm.tm_isdst;

         tm.tm_wday = -1;
         mktime(&tm);
         if (tm.tm_wday == -1)
             goto out;

         if (tm.tm_isdst != tp->tm_isdst) {
             errno = ENOTUNIQ;
             return t;
         }
     out:
         errno = e;
         return t;
     }

Cheers,
Alex


Range-diff against v3:
1:  e9e31a505 < -:  --------- ctime.3: EXAMPLES: Add example program
-:  --------- > 1:  b7ed55965 ctime.3: Document how to detect invalid or ambiguous times

 man/man3/ctime.3 | 93 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 92 insertions(+), 1 deletion(-)

diff --git a/man/man3/ctime.3 b/man/man3/ctime.3
index 0ad2b530f..934da149e 100644
--- a/man/man3/ctime.3
+++ b/man/man3/ctime.3
@@ -427,7 +427,30 @@ .SS mktime()
 .I tm->tm_wday
 field.
 See the example program in EXAMPLES.
+.P
+Passing an invalid time to
+.BR mktime ()
+or an invalid
+.I tm->tm_isdst
+value
+yields an unspecified result.
+Also,
+passing the value
+.I \-1
+in
+.I tm->tm_isdst
+will result in an ambiguous time during some DST transitions,
+which will also yield an unspecified result.
+See the example program in EXAMPLES.
 .SH EXAMPLES
+The program below defines a wrapper that
+allows detecting invalid and ambiguous times,
+with
+.B EINVAL
+and
+.BR ENOTUNIQ ,
+respectively.
+.P
 The following shell session shows sample runs of the program:
 .P
 .in +4n
@@ -443,6 +466,7 @@ .SH EXAMPLES
 .RB $\~ "./a.out 2024 08 23 00 17 53 \-1" ;
 1724365073
 .RB $\~ "./a.out 2024 08 23 00 17 53 0" ;
+a.out: mktime: Invalid argument
 1724368673
 .RB $\~ "./a.out 2024 08 23 00 17 53 1" ;
 1724365073
@@ -452,12 +476,15 @@ .SH EXAMPLES
 .RB $\~ "./a.out 2024 02 23 00 17 53 0" ;
 1708643873
 .RB $\~ "./a.out 2024 02 23 00 17 53 1" ;
+a.out: mktime: Invalid argument
 1708640273
 $
 .RB $\~ "./a.out 2023 03 26 02 17 53 \-1" ;
+a.out: mktime: Invalid argument
 1679793473
 $
 .RB $\~ "./a.out 2023 10 29 02 17 53 \-1" ;
+a.out: mktime: Name not unique on network
 1698542273
 .RB $\~ "./a.out 2023 10 29 02 17 53 0" ;
 1698542273
@@ -465,6 +492,7 @@ .SH EXAMPLES
 1698538673
 $
 .RB $\~ "./a.out 2023 02 29 12 00 00 \-1" ;
+a.out: mktime: Invalid argument
 1677668400
 .EE
 .SS Program source: mktime.c
@@ -472,13 +500,17 @@ .SS Program source: mktime.c
 .\" SRC BEGIN (mktime.c)
 .EX
 #include <err.h>
+#include <errno.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 #include <time.h>
 \&
 #define is_signed(T)  ((T) \-1 < 1)
 \&
+time_t my_mktime(struct tm *tp);
+\&
 int
 main(int argc, char *argv[])
 {
@@ -500,10 +532,13 @@ .SS Program source: mktime.c
     tm.tm_sec   = atoi(*p++);
     tm.tm_isdst = atoi(*p++);
 \&
+    errno = 0;
     tm.tm_wday = \-1;
-    t = mktime(&tm);
+    t = my_mktime(&tm);
     if (tm.tm_wday == \-1)
         err(EXIT_FAILURE, "mktime");
+    if (errno == EINVAL || errno == ENOTUNIQ)
+        warn("mktime");
 \&
     if (is_signed(time_t))
         printf("%jd\[rs]n", (intmax_t) t);
@@ -512,6 +547,62 @@ .SS Program source: mktime.c
 \&
     exit(EXIT_SUCCESS);
 }
+\&
+time_t
+my_mktime(struct tm *tp)
+{
+    int            e, isdst;
+    time_t         t;
+    struct tm      tm;
+    unsigned char  wday[sizeof(tp\->tm_wday)];
+\&
+    e = errno;
+\&
+    tm = *tp;
+    isdst = tp\->tm_isdst;
+\&
+    memcpy(wday, &tp\->tm_wday, sizeof(wday));
+    tp\->tm_wday = \-1;
+    t = mktime(tp);
+    if (tp\->tm_wday == \-1) {
+        memcpy(&tp\->tm_wday, wday, sizeof(wday));
+        return \-1;
+    }
+\&
+    if (isdst == \-1)
+        tm.tm_isdst = tp\->tm_isdst;
+\&
+    if (   tm.tm_sec   != tp\->tm_sec
+        || tm.tm_min   != tp\->tm_min
+        || tm.tm_hour  != tp\->tm_hour
+        || tm.tm_mday  != tp\->tm_mday
+        || tm.tm_mon   != tp\->tm_mon
+        || tm.tm_year  != tp\->tm_year
+        || tm.tm_isdst != tp\->tm_isdst)
+    {
+        errno = EINVAL;
+        return t;
+    }
+\&
+    if (isdst != \-1)
+        goto out;
+\&
+    tm = *tp;
+    tm.tm_isdst = !tm.tm_isdst;
+\&
+    tm.tm_wday = \-1;
+    mktime(&tm);
+    if (tm.tm_wday == \-1)
+        goto out;
+\&
+    if (tm.tm_isdst != tp\->tm_isdst) {
+        errno = ENOTUNIQ;
+        return t;
+    }
+out:
+    errno = e;
+    return t;
+}
 .EE
 .\" SRC END
 .SH SEE ALSO

base-commit: 6a7f1461b0e5474d50ef1920558dec103c0c058f
-- 
2.45.2

Attachment: signature.asc
Description: PGP signature


[Index of Archives]     [Kernel Documentation]     [Netdev]     [Linux Ethernet Bridging]     [Linux Wireless]     [Kernel Newbies]     [Security]     [Linux for Hams]     [Netfilter]     [Bugtraq]     [Yosemite News]     [MIPS Linux]     [ARM Linux]     [Linux RAID]     [Linux Admin]     [Samba]

  Powered by Linux