2

I'm trying to print out the date, in a human readable format, for the maximum time_t value possible. The following code seems to work just fine on 32bits machines (initializing m_time with 0x7fffffff), but it outputs null for the theoretically highest value on a 64bits machine. Is that a ctime limitation or am I missing something?

compilation : gcc -Wall -g3 main.c -o time_test
host machine: x86_64.

#include <stdio.h>
#include <time.h>
#include <stddef.h>

int main(int argc, char** argv) {
  time_t m_time = 0x7fffffffffffffff;
  time_t current_time;
  time(&current_time);

  printf("time_t info: sizeof [%ld] bytes or [%ld] bits.\n", sizeof(time_t), sizeof(time_t) *8 );
  printf("m_time val: [%ld]-> %s\n", m_time, ctime(&m_time)); 
  printf("current_time val: [%ld]-> %s\n", current_time, ctime(&current_time));
  return 0;
}

Output:

time_t info: sizeof [8] bytes or [64] bits.
m_time val: [9223372036854775807]-> (null)
current_time val: [1430678274]-> Sun May  3 15:37:54 2015

tks.

Thomas Dickey
  • 51,086
  • 7
  • 70
  • 105
pbn
  • 112
  • 10
  • 1
    "When encountering an error, these functions return `NULL` and set `errno` to an appropriate value." What does `perror` say? – zneak May 03 '15 at 18:38
  • Indeed there is an error. Error: Invalid argument. – pbn May 03 '15 at 18:42
  • Pragmatically, if `time_t` has more than 40 bits (e.g. 64 bits) you don't care about the maximally representable time. You and everyone reading that forum (and all our grand grand children) would be dead, the computers running your program will all be destroyed, and at that time C won't exist anymore. The [Y2038 problem](http://en.wikipedia.org/wiki/Year_2038_problem) don't practically have any 64 bits equivalent. So just special case `time_t` is 32 bits. – Basile Starynkevitch May 03 '15 at 18:51
  • Well, it could report a date 292 billion years in the future. Or it could assume there won't be anybody around anymore to run your program. What would you do? – Hans Passant May 03 '15 at 18:57
  • @BasileStarynkevitch, but your code can be so, so good, that it survives the pass of time, even to that time. Just kidding, don't flame me :) – Luis Colorado May 04 '15 at 05:52

2 Answers2

4

BTW, ctime (& ctime(3)) is documented as giving a string with the year represented by four digits (for a total of 26 bytes). So the maximal time is in the year 9999 (certainly less than maximal time_t on a machine with 64 bits time_t).

Also (as I commented), pragmatically, if time_t has more than 40 bits (e.g. 64 bits) you don't care about the maximally representable time. You and everyone reading that forum (and all our grand grand children) would be dead, the computers running your program will all be destroyed, and at that time C won't exist anymore. The Y2038 problem don't practically have any 64 bits equivalent. So just special case when time_t is 32 bits.

It is very unlikely than any C program would matter after the year 3000; software, hardware, standards, and human technical expertise don't last that long...

The POSIX ctime documentation says explicitly :

Attempts to use ctime() or ctime_r() for times before the Epoch or for times beyond the year 9999 produce undefined results. Refer to asctime.

BTW, musl-libc seems to be conformant to the standard: its time/__asctime.c (indirectly called by ctime) has a nice comment:

if (snprintf(buf, 26, "%.3s %.3s%3d %.2d:%.2d:%.2d %d\n",
    __nl_langinfo(ABDAY_1+tm->tm_wday),
    __nl_langinfo(ABMON_1+tm->tm_mon),
    tm->tm_mday, tm->tm_hour,
    tm->tm_min, tm->tm_sec,
    1900 + tm->tm_year) >= 26)
{
    /* ISO C requires us to use the above format string,
     * even if it will not fit in the buffer. Thus asctime_r
     * is _supposed_ to crash if the fields in tm are too large.
     * We follow this behavior and crash "gracefully" to warn
     * application developers that they may not be so lucky
     * on other implementations (e.g. stack smashing..).
     */
    a_crash();
}

and GNU glibc has in its time/asctime.c file:

/* We limit the size of the year which can be printed.  Using the %d
   format specifier used the addition of 1900 would overflow the
   number and a negative vaue is printed.  For some architectures we
   could in theory use %ld or an evern larger integer format but
   this would mean the output needs more space.  This would not be a
   problem if the 'asctime_r' interface would be defined sanely and
   a buffer size would be passed.  */
if (__glibc_unlikely (tp->tm_year > INT_MAX - 1900))
  {
  eoverflow:
    __set_errno (EOVERFLOW);
    return NULL;
  }

int n = __snprintf (buf, buflen, format,
          (tp->tm_wday < 0 || tp->tm_wday >= 7 ?
           "???" : ab_day_name (tp->tm_wday)),
          (tp->tm_mon < 0 || tp->tm_mon >= 12 ?
           "???" : ab_month_name (tp->tm_mon)),
          tp->tm_mday, tp->tm_hour, tp->tm_min,
            tp->tm_sec, 1900 + tp->tm_year);
if (n < 0)
 return NULL;
if (n >= buflen)
  goto eoverflow;

So I believe that both GNU glibc and musl-libc are better than MacOSX implementation (as cited in zneak's answer) on that aspect. The standards requires ctime to give 26 bytes. Also, POSIX 2008 is marking ctime as obsolete, new code should use strftime (see also strftime(3)).

Community
  • 1
  • 1
Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • Weird, [`asctime`](http://pubs.opengroup.org/onlinepubs/9699919799/functions/asctime.html#) (same reference) makes no mention to year 9999, and instead says that the result is undefined if `tm_year` is larger than `{INT_MAX}-1990`. What may cause UB is that the result buffer is defined to be 26 bytes. This is not an issue with `asctime_r`, and should neither be with `ctime_r`. – zneak May 03 '15 at 19:16
2

To get to the bottom of this, the best idea is to find an implementation and look at what it does. I downloaded Apple's Libc tarball for OS X 10.1.1 (whose link can be found on this page), and found that ctime is defined in stdtime/FreeBSD/localtime.c.

The function goes like this:

char *
ctime(timep)
const time_t * const    timep;
{
/*
** Section 4.12.3.2 of X3.159-1989 requires that
**  The ctime function converts the calendar time pointed to by timer
**  to local time in the form of a string.  It is equivalent to
**      asctime(localtime(timer))
*/
#ifdef __LP64__
    /*
     * In 64-bit, the timep value may produce a time value with a year
     * that exceeds 32-bits in size (won't fit in struct tm), so localtime
     * will return NULL.
     */
    struct tm *tm = localtime(timep);

    if (tm == NULL)
        return NULL;
    return asctime(tm);
#else /* !__LP64__ */
    return asctime(localtime(timep));
#endif /* __LP64__ */
}

From a second-hand reference, struct tm appears to be defined in terms of integers, and the tm_year field is an offset from 1900. Assuming conformance to that, even a non-conforming ctime cannot possibly accept a timestamp after year 231+1900-1.

Here is a program that finds (and tests) the largest timestamp ctime will accept with Apple's implementation:

#include <limits.h>
#include <stdio.h>
#include <time.h>

int main(int argc, char** argv) {
    struct tm t = {
        .tm_sec = 59,
        .tm_min = 59,
        .tm_hour = 23,
        .tm_mday = 31,
        .tm_mon = 11,
        .tm_year = INT_MAX,
    };
    time_t max = mktime(&t);

    printf("Maximum time: %li\n", max);
    printf("ctime max: %s\n", ctime(&max));
    max++;
    printf("ctime max+1: %s\n", ctime(&max));
}

Output:

Maximum time: 67768036191694799
ctime max: Wed Dec 31 23:59:59 2147485547
ctime max+1: (null)

This is a 56-bit number, so the maximum year a 64-bit time_t can hold (though struct tm can't) is probably between 547,608,814,485 and 549,756,300,032, or like 36 times the age of the universe. In other words, it's going to be a while.

For what it's worth, Apple's implementation is not conforming. The standard says that the output of ctime has to fit inside 26 bytes, including a newline character and a null character. For a conforming implementation, this means that the year has to be within -999 and 9999.

zneak
  • 134,922
  • 42
  • 253
  • 328