chiark / gitweb /
More portable handling of SOURCE_DATE_EPOCH
authorBen Harris <bjh21@bjh21.me.uk>
Tue, 4 Feb 2025 23:11:17 +0000 (23:11 +0000)
committerBen Harris <bjh21@bjh21.me.uk>
Tue, 4 Feb 2025 23:11:17 +0000 (23:11 +0000)
Now we don't make any assumption about being on a POSIX system, and
instead have our own implementation of gmtime().  This turns out to be
shorter than the comment explaining why the previous code was more or
less valid.

While I'm in the area, also fail if the year is after 9999, since
asctime() causes undefined behaviour in those cases.

bedstead.c

index fbeee78937889bba87d34a2d89c1035db95bde29..9bdc44de273f04307b4b6b5302de2efd4dc58d82 100644 (file)
@@ -3144,79 +3144,64 @@ fullname_to_fontname(char const *fullname)
 static char *
 time_for_ttx(void)
 {
-       time_t now = 0;
+       struct tm *timeptr, tm;
        uintmax_t epochumax;
-       struct tm *timeptr;
        char *epochstr, *endptr, *timestr;
 
        /* Work out what timestamp to use. */
        if ((epochstr = getenv("SOURCE_DATE_EPOCH")) != NULL) {
-               /*
-                * Correctly handling SOURCE_DATE_EPOCH is
-                * surprisingly fiddly.  To make life slightly easy,
-                * we assume that we're on a POSIX system, where
-                * time_t is an integer type, and that we can reject
-                * negative values out of hand.
-                *
-                * Even given that, time_t might be any integer type.
-                * It might be signed or unsigned, and it might be a
-                * standard or an extended type.
-                *
-                * If time_t is unsigned, it's always safe to convert
-                * something to it.  If the value is within the range
-                * of time_t it will remain unchanged, and if it
-                * isn't, it'll be reduced to be within the range (C11
-                * 6.3.1.3).
-                *
-                * While it's well-known that signed integer overflow
-                * in C causes undefined behaviour, this doesn't apply
-                * to conversion.  According to C11 6.3.1.3, if a
-                * conversion overflows "either the result is
-                * implementation-defined or an implementation-defined
-                * signal is raised."  I can't find any clear
-                * definition of what might happen when such a signal
-                * is raised, but I think it must either cause the
-                * process to exit with an error or do nothing (and
-                * presumably leave the destination untouched).
-                *
-                * So having initialised the destination to 0, we can
-                * assume that after the assignment it will either
-                * have the correct value or have a different value
-                * within the range of time_t.  Then we just have to
-                * check it.
-                */
                errno = 0;
                epochumax = strtoumax(epochstr, &endptr, 10);
                if (!isdigit((unsigned char)epochstr[0]) ||
                    endptr == epochstr || *endptr != '\0' ||
-                   errno == ERANGE) {
+                   errno == ERANGE ||
+                   /* Limit ourselves to 2000 to 10000. */
+                   epochumax < 946684800 || epochumax >= 253402300800) {
                        fprintf(stderr, "Invalid SOURCE_DATE_EPOCH\n");
                        return NULL;
                }
-               now = epochumax;
-               /*
-                * Check if the value fitted into time_t.  If "now" is
-                * negative then it obviously didn't.  If it's
-                * non-negative then converting back into a uintmax_t
-                * will not change the value, since all non-negative
-                * numbers that can be represented in any integer type
-                * can be represented in a uintmax_t.
-                */
-               if (now < 0 || (uintmax_t)now != epochumax) {
-                       fprintf(stderr, "Invalid SOURCE_DATE_EPOCH\n");
-                       return NULL;
+               epochumax -= 946684800; /* Rebase to 2000. */
+               tm.tm_isdst = -1;
+               tm.tm_sec = epochumax % 60;
+               tm.tm_min = epochumax / 60 % 60;
+               tm.tm_hour = epochumax / 3600 % 24;
+               long day = epochumax / 86400;
+               tm.tm_wday = (day - 1) % 7;
+               int y = 2000 + (day / 146097) * 400;
+               day = day % 146097;
+               bool ly;
+               for (;;) {
+                       ly = y % 400 ? y % 100 ? y % 4 ? false:true:false:true;
+                       if (day < 365 + ly) break;
+                       day -= 365 + ly; y++;
                }
+               tm.tm_year = y - 1900;
+               tm.tm_yday = day;
+               static int const mlc[] = {31,28,31,30,31,30,31,31,30,31,30,31};
+               static int const mll[] = {31,29,31,30,31,30,31,31,30,31,30,31};
+               tm.tm_mon = 0;
+               for (;;) {
+                       int md = (ly ? mll : mlc)[tm.tm_mon];
+                       if (day < md) break;
+                       day -= md; tm.tm_mon++;
+               }
+               tm.tm_mday = day + 1;
+               timeptr = &tm;
        } else {
-               now = time(NULL);
+               time_t now = time(NULL);
                if (now == (time_t)-1) {
                        fprintf(stderr, "Can't get current time\n");
                        return NULL;
                }
-       }
-       timeptr = gmtime(&now);
-       if (timeptr == NULL) {
-               fprintf(stderr, "Can't convert time to UTC\n");
-               return NULL;
+               timeptr = gmtime(&now);
+               if (timeptr == NULL) {
+                       fprintf(stderr, "Can't convert time to UTC\n");
+                       return NULL;
+               }
+               if (timeptr->tm_year >= 8100) {
+                       fprintf(stderr, "Can't handle years past 9999\n");
+                       return NULL;
+               }
        }
        timestr = asctime(timeptr);
        assert(strlen(timestr) == 25);