From 30784c47105573c213cda1b3c4f397c8df837966 Mon Sep 17 00:00:00 2001 From: Paul Eggert Date: Sat, 22 Apr 2017 02:11:26 -0700 Subject: [PATCH] parse-datetime: overflow and debug cleanups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This long patch was triggered by this bug report from Ruediger Meier: http://lists.gnu.org/archive/html/bug-gnulib/2017-04/msg00028.html I fixed the bug he noted, then found some others nearby, and then still others. Oh my goodness, there were a lot of bugs. I cleaned up some of the code to follow GNU standards while I was at it. * lib/parse-datetime.y (ISDIGIT): Remove; all callers changed to use c_isdigit. (EPOCH_YEAR): Remove; unused. (TM_YEAR_BASE): Now an enum rather than a macro. (HOUR, debug_strfdatetime): Multiply hour by 3600, not 60, to get time zone offset, since timezones now are in terms of seconds and not minutes. (long_time_t): Remove. All uses replaced by time_t or intmax_t as appropriate. Verify that intmax_t is wide enough. (time_overflow, time_zone_str): New functions, used to deal more reliably with overflow. (dbg_printf): Add printf attribute, to help catch integer width errors. (textint, relative_time, parser_control, time_zone_hhmm, set_hhmmss) (%union, to_hour, yylex, parse_datetime2): Use intmax_t instead of long int and/or long_time_t. All uses changed. (DBGBUFSIZE): Move earlier. (relative_time, set_hhmmss, parser_control): Just use int for nanoseconds and for time zones; that’s wide enough. (parser_control): Use bool for members like year_seen that can be booleans instead of counters. All uses changed. Remove debug_default_input_timezone; no longer needed. All uses removed. (apply_relative_time): Return a bool overflow flag. All uses changed to check for overflow. (apply_relative_time, zone, date, relunit, relunit_snumber) (signed_seconds, unsigned_seconds, yylex, parse_datetime2): Check for integer overflow portably. (str_days): Use just int for N, as it’s wide enough. Prefer 2D char arrays to arrays of char * when it looks like 2D is a win on typical platforms. Prefer snprintf to strncpy/strncat, for simplicity; all buffers are smaller than INT_MAX so this is safe. (TIME_ZONE_BUFSiZE, TM_YEAR_BUFSIZE): New constants. (debug_print_current_time): Don’t assume tv_nsec is of type long, as this is not true on x32. Output "." before any nanoseconds. (debug_print_current_time, parse_datetime2): Output local zones using a more-consistent format. (debug_print_current_time, date, parse_datetime2): (main) [TEST]: Don’t assume time_t is the same width as long. (print_rel_part): New function, replacing ... (PRINT_REL_PART): ... this macro, which was removed. All uses changed. (debug_print_relative_time): Use bool for boolean. (local_zone): dsts_seen now counts only tDST instances. (date): Fix printf of size_t to use %z. Do not assume numeric tokens have negative values merely because the context suggests a syntax with "-" separating tokens. (time_zone_hhmm): Return bool success indicator, which checks for overflow. Store result into PC->time_zone instead. All callers changed. (tm_year_str): New function. Return a bool success indicator and store the result into a buffer. All callers changed. Output the numerically correct string even if adding 1900 to the year would overflow. (to_tm_year): New function, replacing the old to_year. All callers changed. (tm_diff): Sync with glibc. (lookup_word): Use to_uchar instead of doing it by hand. (TZBUFSIZE): Now local to the only function that needs it. (debug_strfdatetime): Simplify now that time zones are int seconds. (debug_strfdate): Work even if tm_year + 1900 would overflow. (get_effective_timezone): Remove. All uses removed. (parse_datetime2): Use fprintf in pieces instead of snprintfing to a fixed-size buffer. Don’t assume that gmtime succeeds iff localtime succeeds. Use tm_gmtoff if available. Simplify how ‘goto fail;’ works in conjunction with the ‘ok’ flag. * m4/parse-datetime.m4 (gl_PARSE_DATETIME): Don’t define TIME_T_FITS_IN_LONG_INT, as it is no longer needed. * modules/parse-datetime (Depends-on): Add inttypes. --- ChangeLog | 79 +++ lib/parse-datetime.y | 1307 +++++++++++++++++++++++------------------------- m4/parse-datetime.m4 | 18 +- modules/parse-datetime | 1 + 4 files changed, 720 insertions(+), 685 deletions(-) diff --git a/ChangeLog b/ChangeLog index e555d07..9cf5636 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,82 @@ +2017-04-22 Paul Eggert + + parse-datetime: overflow and debug cleanups + This long patch was triggered by this bug report from Ruediger Meier: + http://lists.gnu.org/archive/html/bug-gnulib/2017-04/msg00028.html + I fixed the bug he noted, then found some others nearby, and then + still others. Oh my goodness, there were a lot of bugs. I cleaned + up some of the code to follow GNU standards while I was at it. + * lib/parse-datetime.y (ISDIGIT): Remove; all callers changed to + use c_isdigit. + (EPOCH_YEAR): Remove; unused. + (TM_YEAR_BASE): Now an enum rather than a macro. + (HOUR, debug_strfdatetime): Multiply hour by 3600, not 60, to get + time zone offset, since timezones now are in terms of seconds and + not minutes. + (long_time_t): Remove. All uses replaced by time_t or intmax_t as + appropriate. Verify that intmax_t is wide enough. + (time_overflow, time_zone_str): New functions, used to deal + more reliably with overflow. + (dbg_printf): Add printf attribute, to help catch integer width errors. + (textint, relative_time, parser_control, time_zone_hhmm, set_hhmmss) + (%union, to_hour, yylex, parse_datetime2): + Use intmax_t instead of long int and/or long_time_t. + All uses changed. + (DBGBUFSIZE): Move earlier. + (relative_time, set_hhmmss, parser_control): + Just use int for nanoseconds and for time zones; that’s wide enough. + (parser_control): Use bool for members like year_seen that can + be booleans instead of counters. All uses changed. + Remove debug_default_input_timezone; no longer needed. + All uses removed. + (apply_relative_time): Return a bool overflow flag. + All uses changed to check for overflow. + (apply_relative_time, zone, date, relunit, relunit_snumber) + (signed_seconds, unsigned_seconds, yylex, parse_datetime2): + Check for integer overflow portably. + (str_days): Use just int for N, as it’s wide enough. + Prefer 2D char arrays to arrays of char * when it looks like + 2D is a win on typical platforms. + Prefer snprintf to strncpy/strncat, for simplicity; + all buffers are smaller than INT_MAX so this is safe. + (TIME_ZONE_BUFSiZE, TM_YEAR_BUFSIZE): New constants. + (debug_print_current_time): Don’t assume tv_nsec is of type long, + as this is not true on x32. Output "." before any nanoseconds. + (debug_print_current_time, parse_datetime2): + Output local zones using a more-consistent format. + (debug_print_current_time, date, parse_datetime2): + (main) [TEST]: + Don’t assume time_t is the same width as long. + (print_rel_part): New function, replacing ... + (PRINT_REL_PART): ... this macro, which was removed. All uses changed. + (debug_print_relative_time): Use bool for boolean. + (local_zone): dsts_seen now counts only tDST instances. + (date): Fix printf of size_t to use %z. Do not assume numeric + tokens have negative values merely because the context suggests + a syntax with "-" separating tokens. + (time_zone_hhmm): Return bool success indicator, which checks for + overflow. Store result into PC->time_zone instead. All callers + changed. + (tm_year_str): New function. Return a bool success indicator and + store the result into a buffer. All callers changed. Output the + numerically correct string even if adding 1900 to the year would + overflow. + (to_tm_year): New function, replacing the old to_year. All + callers changed. + (tm_diff): Sync with glibc. + (lookup_word): Use to_uchar instead of doing it by hand. + (TZBUFSIZE): Now local to the only function that needs it. + (debug_strfdatetime): Simplify now that time zones are int seconds. + (debug_strfdate): Work even if tm_year + 1900 would overflow. + (get_effective_timezone): Remove. All uses removed. + (parse_datetime2): Use fprintf in pieces instead of snprintfing + to a fixed-size buffer. Don’t assume that gmtime succeeds iff + localtime succeeds. Use tm_gmtoff if available. Simplify how + ‘goto fail;’ works in conjunction with the ‘ok’ flag. + * m4/parse-datetime.m4 (gl_PARSE_DATETIME): Don’t define + TIME_T_FITS_IN_LONG_INT, as it is no longer needed. + * modules/parse-datetime (Depends-on): Add inttypes. + 2017-04-21 Bruno Haible gettext-h: Avoid -Wundef warning. diff --git a/lib/parse-datetime.y b/lib/parse-datetime.y index 6af4f29..3b815a9 100644 --- a/lib/parse-datetime.y +++ b/lib/parse-datetime.y @@ -21,14 +21,15 @@ a couple of people on Usenet. Completely overhauled by Rich $alz and Jim Berets in August, 1990. - Modified by Paul Eggert in August 1999 to do - the right thing about local DST. Also modified by Paul Eggert - in February 2004 to support - nanosecond-resolution timestamps, and in October 2004 to support - TZ strings in dates. */ + Modified by Assaf Gordon in 2016 to add + debug output. -/* FIXME: Check for arithmetic overflow in all cases, not just - some of them. */ + Modified by Paul Eggert in 1999 to do the + right thing about local DST. Also modified by Paul Eggert + in 2004 to support nanosecond-resolution + timestamps, in 2004 to support TZ strings in dates, and in 2017 to + check for integer overflow and to support longer-than-'long' + 'time_t' and 'tv_nsec'. */ #include @@ -80,15 +81,6 @@ # define _STDLIB_H 1 #endif -/* ISDIGIT differs from isdigit, as follows: - - Its arg may be any int or unsigned int; it need not be an unsigned char - or EOF. - - It's typically faster. - POSIX says that only '0' through '9' are digits. Prefer ISDIGIT to - isdigit unless it's important to use the locale's definition - of "digit" even when the host does not conform to POSIX. */ -#define ISDIGIT(c) ((unsigned int) (c) - '0' <= 9) - /* Shift A right by B bits portably, by dividing A by 2**B and truncating towards minus infinity. A and B should be free of side effects, and B should be in the range 0 <= B <= INT_BITS - 2, where @@ -104,31 +96,35 @@ ? (a) >> (b) \ : (a) / (1 << (b)) - ((a) % (1 << (b)) < 0)) -#define EPOCH_YEAR 1970 -#define TM_YEAR_BASE 1900 - -#define HOUR(x) ((x) * 60) +#define HOUR(x) (60 * 60 * (x)) #define STREQ(a, b) (strcmp (a, b) == 0) -/* long_time_t is a signed integer type that contains all time_t values. */ +/* Verify that time_t is an integer as POSIX requires, and that every + time_t value fits in intmax_t. Please file a bug report if these + assumptions are false on your platform. */ verify (TYPE_IS_INTEGER (time_t)); -#if TIME_T_FITS_IN_LONG_INT -typedef long int long_time_t; -#else -typedef time_t long_time_t; -#endif +verify (!TYPE_SIGNED (time_t) || INTMAX_MIN <= TYPE_MINIMUM (time_t)); +verify (TYPE_MAXIMUM (time_t) <= INTMAX_MAX); + +/* True if N is out of range for time_t. */ +static bool +time_overflow (intmax_t n) +{ + return ! ((TYPE_SIGNED (time_t) ? TYPE_MINIMUM (time_t) <= n : 0 <= n) + && n <= TYPE_MAXIMUM (time_t)); +} /* Convert a possibly-signed character to an unsigned character. This is a bit safer than casting to unsigned char, since it catches some type errors that the cast doesn't. */ static unsigned char to_uchar (char ch) { return ch; } -static void -dbg_printf (const char *msg,...) +static void _GL_ATTRIBUTE_FORMAT ((__printf__, 1, 2)) +dbg_printf (char const *msg, ...) { va_list args; - /* TODO: use gnulib's 'program_name' instead? */ + /* TODO: use gnulib's 'program_name' instead? */ fputs ("date: ", stderr); va_start (args, msg); @@ -137,21 +133,12 @@ dbg_printf (const char *msg,...) } - -/* Lots of this code assumes time_t and time_t-like values fit into - long_time_t. */ -verify (TYPE_MINIMUM (long_time_t) <= TYPE_MINIMUM (time_t) - && TYPE_MAXIMUM (time_t) <= TYPE_MAXIMUM (long_time_t)); - -/* FIXME: It also assumes that signed integer overflow silently wraps around, - but this is not true any more with recent versions of GCC 4. */ - /* An integer value, and the number of digits in its textual representation. */ typedef struct { bool negative; - long int value; + intmax_t value; size_t digits; } textint; @@ -166,19 +153,22 @@ typedef struct /* Meridian: am, pm, or 24-hour style. */ enum { MERam, MERpm, MER24 }; +/* A reasonable upper bound for the buffer used in debug output. */ +enum { DBGBUFSIZE = 100 }; + enum { BILLION = 1000000000, LOG10_BILLION = 9 }; /* Relative times. */ typedef struct { /* Relative year, month, day, hour, minutes, seconds, and nanoseconds. */ - long int year; - long int month; - long int day; - long int hour; - long int minutes; - long_time_t seconds; - long int ns; + intmax_t year; + intmax_t month; + intmax_t day; + intmax_t hour; + intmax_t minutes; + intmax_t seconds; + int ns; } relative_time; #if HAVE_COMPOUND_LITERALS @@ -190,11 +180,11 @@ static relative_time const RELATIVE_TIME_0; /* Information passed to and from the parser. */ typedef struct { - /* The input string remaining to be parsed. */ + /* The input string remaining to be parsed. */ const char *input; /* N, if this is the Nth Tuesday. */ - long int day_ordinal; + intmax_t day_ordinal; /* Day of week; Sunday is 0. */ int day_number; @@ -202,18 +192,18 @@ typedef struct /* tm_isdst flag for the local zone. */ int local_isdst; - /* Time zone, in minutes east of UTC. */ - long int time_zone; + /* Time zone, in seconds east of UT. */ + int time_zone; /* Style used for time. */ int meridian; /* Gregorian year, month, day, hour, minutes, seconds, and nanoseconds. */ textint year; - long int month; - long int day; - long int hour; - long int minutes; + intmax_t month; + intmax_t day; + intmax_t hour; + intmax_t minutes; struct timespec seconds; /* includes nanoseconds */ /* Relative year, month, day, hour, minutes, seconds, and nanoseconds. */ @@ -228,26 +218,22 @@ typedef struct size_t dsts_seen; size_t times_seen; size_t zones_seen; - size_t year_seen; + bool year_seen; - /* if true, print debugging output to stderr */ + /* Print debugging output to stderr. */ bool parse_datetime_debug; - /* which of the 'seen' parts has been printed when debugging */ - size_t debug_dates_seen; - size_t debug_days_seen; - size_t debug_local_zones_seen; - size_t debug_dsts_seen; - size_t debug_times_seen; - size_t debug_zones_seen; - size_t debug_year_seen; + /* Which of the 'seen' parts have been printed when debugging. */ + bool debug_dates_seen; + bool debug_days_seen; + bool debug_local_zones_seen; + bool debug_times_seen; + bool debug_zones_seen; + bool debug_year_seen; - /* true if the user specified explicit ordinal day value, */ + /* The user specified explicit ordinal day value. */ bool debug_ordinal_day_seen; - /* the default input timezone, set by TZ value */ - long int debug_default_input_timezone; - /* Table of local time zone abbreviations, terminated by a null entry. */ table local_time_zone_table[3]; } parser_control; @@ -255,7 +241,7 @@ typedef struct union YYSTYPE; static int yylex (union YYSTYPE *, parser_control *); static int yyerror (parser_control const *, char const *); -static long int time_zone_hhmm (parser_control *, textint, long int); +static bool time_zone_hhmm (parser_control *, textint, intmax_t); /* Extract into *PC any date and time info from a string of digits of the form e.g., YYYYMMDD, YYMMDD, HHMM, HH (and sometimes YYY, @@ -266,7 +252,7 @@ digits_to_date_time (parser_control *pc, textint text_int) if (pc->dates_seen && ! pc->year.digits && ! pc->rels_seen && (pc->times_seen || 2 < text_int.digits)) { - pc->year_seen++; + pc->year_seen = true; pc->year = text_int; } else @@ -299,24 +285,35 @@ digits_to_date_time (parser_control *pc, textint text_int) } } -/* Increment PC->rel by FACTOR * REL (FACTOR is 1 or -1). */ -static void +/* Increment PC->rel by FACTOR * REL (FACTOR is 1 or -1). Return true + if successful, false if an overflow occurred. */ +static bool apply_relative_time (parser_control *pc, relative_time rel, int factor) { - pc->rel.ns += factor * rel.ns; - pc->rel.seconds += factor * rel.seconds; - pc->rel.minutes += factor * rel.minutes; - pc->rel.hour += factor * rel.hour; - pc->rel.day += factor * rel.day; - pc->rel.month += factor * rel.month; - pc->rel.year += factor * rel.year; + if (factor < 0 + ? (INT_SUBTRACT_WRAPV (pc->rel.ns, rel.ns, &pc->rel.ns) + | INT_SUBTRACT_WRAPV (pc->rel.seconds, rel.seconds, &pc->rel.seconds) + | INT_SUBTRACT_WRAPV (pc->rel.minutes, rel.minutes, &pc->rel.minutes) + | INT_SUBTRACT_WRAPV (pc->rel.hour, rel.hour, &pc->rel.hour) + | INT_SUBTRACT_WRAPV (pc->rel.day, rel.day, &pc->rel.day) + | INT_SUBTRACT_WRAPV (pc->rel.month, rel.month, &pc->rel.month) + | INT_SUBTRACT_WRAPV (pc->rel.year, rel.year, &pc->rel.year)) + : (INT_ADD_WRAPV (pc->rel.ns, rel.ns, &pc->rel.ns) + | INT_ADD_WRAPV (pc->rel.seconds, rel.seconds, &pc->rel.seconds) + | INT_ADD_WRAPV (pc->rel.minutes, rel.minutes, &pc->rel.minutes) + | INT_ADD_WRAPV (pc->rel.hour, rel.hour, &pc->rel.hour) + | INT_ADD_WRAPV (pc->rel.day, rel.day, &pc->rel.day) + | INT_ADD_WRAPV (pc->rel.month, rel.month, &pc->rel.month) + | INT_ADD_WRAPV (pc->rel.year, rel.year, &pc->rel.year))) + return false; pc->rels_seen = true; + return true; } /* Set PC-> hour, minutes, seconds and nanoseconds members from arguments. */ static void -set_hhmmss (parser_control *pc, long int hour, long int minutes, - time_t sec, long int nsec) +set_hhmmss (parser_control *pc, intmax_t hour, intmax_t minutes, + time_t sec, int nsec) { pc->hour = hour; pc->minutes = minutes; @@ -324,17 +321,17 @@ set_hhmmss (parser_control *pc, long int hour, long int minutes, pc->seconds.tv_nsec = nsec; } -/* returns a textual representation of the day ordinal/number values - in the parser_control struct (e.g. 'last wed', 'this tues', 'thu') */ -static const char* -str_days (parser_control *pc, char* /*output*/ buffer, size_t n) +/* Return a textual representation of the day ordinal/number values + in the parser_control struct (e.g., "last wed", "this tues", "thu"). */ +static const char * +str_days (parser_control *pc, char *buffer, int n) { - /* TODO: use the relative_time_table[] for reverse lookup */ - static const char* ordinal_values[] = { + /* TODO: use relative_time_table for reverse lookup. */ + static char const ordinal_values[][11] = { "last", "this", "next/first", - "(SECOND)", /* SECOND is commented out in relative_time_table[] */ + "(SECOND)", /* SECOND is commented out in relative_time_table. */ "third", "fourth", "fifth", @@ -344,9 +341,10 @@ str_days (parser_control *pc, char* /*output*/ buffer, size_t n) "ninth", "tenth", "eleventh", - "twelfth"}; + "twelfth" + }; - static const char* days_values[] = { + static char const days_values[][4] = { "Sun", "Mon", "Tue", @@ -354,39 +352,29 @@ str_days (parser_control *pc, char* /*output*/ buffer, size_t n) "Thu", "Fri", "Sat" - }; + }; - /* don't add an ordinal prefix if the user didn't specify it - (e.g., "this wed" vs "wed") */ + int len; + + /* Don't add an ordinal prefix if the user didn't specify it + (e.g., "this wed" vs "wed"). */ if (pc->debug_ordinal_day_seen) { - /* use word description of possible (e.g. -1 = last, 3 = third) */ - if (pc->day_ordinal>=-1 && pc->day_ordinal <=12) - { - strncpy (buffer, ordinal_values[ pc->day_ordinal+1 ], n); - buffer[n-1]='\0'; - } - else - { - snprintf (buffer,n,"%ld",pc->day_ordinal); - } + /* Use word description if possible (e.g., -1 = last, 3 = third). */ + len = (-1 <= pc->day_ordinal && pc->day_ordinal <= 12 + ? snprintf (buffer, n, "%s", ordinal_values[pc->day_ordinal + 1]) + : snprintf (buffer, n, "%"PRIdMAX, pc->day_ordinal)); } else { buffer[0] = '\0'; + len = 0; } /* Add the day name */ - if (pc->day_number>=0 && pc->day_number<=6) - { - size_t l = strlen (buffer); - if (l>0) - { - strncat (buffer," ",n-l); - ++l; - } - strncat (buffer,days_values[pc->day_number],n-l); - } + if (0 <= pc->day_number && pc->day_number <= 6 && 0 <= len && len < n) + snprintf (buffer + len, n - len, &" %s"[len == 0], + days_values[pc->day_number]); else { /* invalid day_number value - should never happen */ @@ -394,14 +382,43 @@ str_days (parser_control *pc, char* /*output*/ buffer, size_t n) return buffer; } +/* Convert a time zone to its string representation. */ + +enum { TIME_ZONE_BUFSIZE = INT_STRLEN_BOUND (intmax_t) + sizeof ":MM:SS" } ; + +static char const * +time_zone_str (int time_zone, char time_zone_buf[TIME_ZONE_BUFSIZE]) +{ + char *p = time_zone_buf; + char sign = time_zone < 0 ? '-' : '+'; + int hour = abs (time_zone / (60 * 60)); + p += sprintf (time_zone_buf, "%c%02d", sign, hour); + int offset_from_hour = abs (time_zone % (60 * 60)); + if (offset_from_hour != 0) + { + int mm = offset_from_hour / 60; + int ss = offset_from_hour % 60; + *p++ = ':'; + *p++ = '0' + mm / 10; + *p++ = '0' + mm % 10; + if (ss) + { + *p++ = ':'; + *p++ = '0' + ss / 10; + *p++ = '0' + ss % 10; + } + *p = '\0'; + } + return time_zone_buf; +} + /* debugging: print the current time in the parser_control structure. The parser will increment "*_seen" members for those which were parsed. - This function will print only newly seen parts. */ + This function will print only newly seen parts. */ static void -debug_print_current_time (const char* item, parser_control *pc) +debug_print_current_time (char const *item, parser_control *pc) { - char tmp[100] = {0}; - int space = 0; /* if true, add space delimiter */ + bool space = false; if (!pc->parse_datetime_debug) return; @@ -409,100 +426,99 @@ debug_print_current_time (const char* item, parser_control *pc) /* no newline, more items printed below */ dbg_printf (_("parsed %s part: "), item); - if (pc->dates_seen != pc->debug_dates_seen) + if (pc->dates_seen && !pc->debug_dates_seen) { - /*TODO: use pc->year.negative? */ - fprintf (stderr,"(Y-M-D) %04ld-%02ld-%02ld", + /*TODO: use pc->year.negative? */ + fprintf (stderr, "(Y-M-D) %04"PRIdMAX"-%02"PRIdMAX"-%02"PRIdMAX, pc->year.value, pc->month, pc->day); - pc->debug_dates_seen = pc->dates_seen; - space = 1; + pc->debug_dates_seen = true; + space = true; } if (pc->year_seen != pc->debug_year_seen) { if (space) - fputc (' ',stderr); - fprintf (stderr, _("year: %04ld"), pc->year.value); + fputc (' ', stderr); + fprintf (stderr, _("year: %04"PRIdMAX), pc->year.value); pc->debug_year_seen = pc->year_seen; - space = 1; + space = true; } - if (pc->times_seen != pc->debug_times_seen) + if (pc->times_seen && !pc->debug_times_seen) { - if (space) - fputc (' ',stderr); - fprintf (stderr,"%02ld:%02ld:%02ld", - pc->hour, pc->minutes, pc->seconds.tv_sec); - if (pc->seconds.tv_nsec!=0) - fprintf (stderr,"%09ld", pc->seconds.tv_nsec); - if (pc->meridian==MERpm) - fputs ("pm",stderr); - - pc->debug_times_seen = pc->times_seen; - space = 1; - } + intmax_t sec = pc->seconds.tv_sec; + fprintf (stderr, &" %02"PRIdMAX":%02"PRIdMAX":%02"PRIdMAX[!space], + pc->hour, pc->minutes, sec); + if (pc->seconds.tv_nsec != 0) + { + int nsec = pc->seconds.tv_nsec; + fprintf (stderr, ".%09d", nsec); + } + if (pc->meridian == MERpm) + fputs ("pm", stderr); - if (pc->days_seen != pc->debug_days_seen) - { - if (space) - fputc (' ',stderr); - fprintf (stderr,_("%s (day ordinal=%ld number=%d)"), - str_days (pc,tmp,sizeof (tmp)), - pc->day_ordinal, pc->day_number); - pc->debug_days_seen = pc->days_seen ; - space = 1; + pc->debug_times_seen = true; + space = true; } - /* local zone strings only change the DST settings, - not the timezone value. If seen, inform about the DST. - its value (0 or 1) will be printed in the next 'if' block */ - if (pc->local_zones_seen != pc->debug_local_zones_seen) + if (pc->days_seen && !pc->debug_days_seen) { if (space) - fputc (' ',stderr); - fputs ( (pc->dsts_seen == pc->debug_dsts_seen)? - _("DST unchanged"):_("DST changed:"), - stderr); - pc->debug_local_zones_seen = pc->local_zones_seen; - space = 1; + fputc (' ', stderr); + char tmp[DBGBUFSIZE]; + fprintf (stderr, _("%s (day ordinal=%"PRIdMAX" number=%d)"), + str_days (pc, tmp, sizeof tmp), + pc->day_ordinal, pc->day_number); + pc->debug_days_seen = true; + space = true; } - if (pc->dsts_seen != pc->debug_dsts_seen) + /* local zone strings only change the DST settings, + not the timezone value. If seen, inform about the DST. */ + if (pc->local_zones_seen && !pc->debug_local_zones_seen) { - if (space) - fputc (' ',stderr); - fprintf (stderr,_("is-dst=%d"), pc->local_isdst); - pc->dsts_seen = pc->debug_dsts_seen; - space = 1; + fprintf (stderr, &" isdst=%d%s"[!space], + pc->local_isdst, pc->dsts_seen ? " DST" : ""); + pc->debug_local_zones_seen = true; + space = true; } - /* TODO: fix incorrect display of EST=2:08h? */ - if (pc->zones_seen != pc->debug_zones_seen) + if (pc->zones_seen && !pc->debug_zones_seen) { - if (space) - fputc (' ',stderr); - fprintf (stderr,_("TZ=%+03d:%02d"), (int)(pc->time_zone/60), - abs ((int)pc->time_zone%60)); - pc->debug_zones_seen = pc->zones_seen; - space = 1; + char time_zone_buf[TIME_ZONE_BUFSIZE]; + fprintf (stderr, &" UTC%s"[!space], + time_zone_str (pc->time_zone, time_zone_buf)); + pc->debug_zones_seen = true; + space = true; } if (pc->timespec_seen) { + intmax_t sec = pc->seconds.tv_sec; if (space) - fputc (' ',stderr); - fprintf (stderr,_("number of seconds: %ld"), pc->seconds.tv_sec); + fputc (' ', stderr); + fprintf (stderr, _("number of seconds: %"PRIdMAX), sec); } fputc ('\n', stderr); } -/* debugging: print the current relative values. */ +/* Debugging: print the current relative values. */ + +static bool +print_rel_part (bool space, intmax_t val, char const *name) +{ + if (val == 0) + return space; + fprintf (stderr, &" %+"PRIdMAX" %s"[!space], val, name); + return true; +} + static void -debug_print_relative_time (const char* item, const parser_control *pc) +debug_print_relative_time (char const *item, parser_control const *pc) { - int space = 0; /* if true, add space delimiter */ + bool space = false; if (!pc->parse_datetime_debug) return; @@ -510,35 +526,24 @@ debug_print_relative_time (const char* item, const parser_control *pc) /* no newline, more items printed below */ dbg_printf (_("parsed %s part: "), item); - if (pc->rel.year==0 && pc->rel.month==0 && pc->rel.day==0 - && pc->rel.hour==0 && pc->rel.minutes==00 && pc->rel.seconds == 0 - && pc->rel.ns==0) + if (pc->rel.year == 0 && pc->rel.month == 0 && pc->rel.day == 0 + && pc->rel.hour == 0 && pc->rel.minutes == 0 && pc->rel.seconds == 0 + && pc->rel.ns == 0) { /* Special case: relative time of this/today/now */ - fputs (_("today/this/now\n"),stderr); - return ; + fputs (_("today/this/now\n"), stderr); + return; } -#define PRINT_REL_PART(x,name) \ - do { \ - if ( (pc->rel.x) != 0 ) \ - { \ - if (space) \ - fputc (' ',stderr); \ - fprintf (stderr,"%+ld %s", pc->rel.x, name); \ - space = 1; \ - } \ - } while (0) - - PRINT_REL_PART (year,"year(s)"); - PRINT_REL_PART (month,"month(s)"); - PRINT_REL_PART (day,"day(s)"); - PRINT_REL_PART (hour,"hour(s)"); - PRINT_REL_PART (minutes,"minutes"); - PRINT_REL_PART (seconds,"seconds"); - PRINT_REL_PART (ns,"nanoseconds"); - - fputc ('\n',stderr); + space = print_rel_part (space, pc->rel.year, "year(s)"); + space = print_rel_part (space, pc->rel.month, "month(s)"); + space = print_rel_part (space, pc->rel.day, "day(s)"); + space = print_rel_part (space, pc->rel.hour, "hour(s)"); + space = print_rel_part (space, pc->rel.minutes, "minutes"); + space = print_rel_part (space, pc->rel.seconds, "seconds"); + print_rel_part (space, pc->rel.ns, "nanoseconds"); + + fputc ('\n', stderr); } @@ -551,12 +556,12 @@ debug_print_relative_time (const char* item, const parser_control *pc) %parse-param { parser_control *pc } %lex-param { parser_control *pc } -/* This grammar has 31 shift/reduce conflicts. */ +/* This grammar has 31 shift/reduce conflicts. */ %expect 31 %union { - long int intval; + intmax_t intval; textint textintval; struct timespec timespec; relative_time rel; @@ -699,63 +704,61 @@ zone_offset: tSNUMBER o_colon_minutes { pc->zones_seen++; - pc->time_zone = time_zone_hhmm (pc, $1, $2); + if (! time_zone_hhmm (pc, $1, $2)) YYABORT; } ; -/* Local zone strings only affect DST setting, - and only take affect if the current TZ setting is relevant. +/* Local zone strings affect only the DST setting, and take effect + only if the current TZ setting is relevant. Example 1: 'EEST' is parsed as tLOCAL_ZONE, as it relates to the effective TZ: - TZ=Europe/Helsinki date -d '2016-12-30 EEST' + TZ='Europe/Helsinki' date -d '2016-06-30 EEST' Example 2: - 'EEST' is parsed as 'zone' (TZ=+03:00): - TZ=Asia/Tokyo ./src/date --debug -d '2011-06-11 EEST' + 'EEST' is parsed as tDAYZONE: + TZ='Asia/Tokyo' date -d '2016-06-30 EEST' This is implemented by probing the next three calendar quarters of the effective timezone and looking for DST changes - if found, the timezone name (EEST) is inserted into the lexical lookup table with type tLOCAL_ZONE. - (Search for 'quarter' comment in 'parse_datetime2'). + (Search for 'quarter' comment in 'parse_datetime2'.) */ local_zone: tLOCAL_ZONE - { - pc->local_isdst = $1; - pc->dsts_seen += (0 < $1); - } + { pc->local_isdst = $1; } | tLOCAL_ZONE tDST { pc->local_isdst = 1; - pc->dsts_seen += (0 < $1) + 1; + pc->dsts_seen++; } ; /* Note 'T' is a special case, as it is used as the separator in ISO - 8601 date and time of day representation. */ + 8601 date and time of day representation. */ zone: tZONE { pc->time_zone = $1; } | 'T' - { pc->time_zone = HOUR(7); } + { pc->time_zone = HOUR (7); } | tZONE relunit_snumber { pc->time_zone = $1; - apply_relative_time (pc, $2, 1); + if (! apply_relative_time (pc, $2, 1)) YYABORT; debug_print_relative_time (_("relative"), pc); } | 'T' relunit_snumber - { pc->time_zone = HOUR(7); - apply_relative_time (pc, $2, 1); + { pc->time_zone = HOUR (7); + if (! apply_relative_time (pc, $2, 1)) YYABORT; debug_print_relative_time (_("relative"), pc); } | tZONE tSNUMBER o_colon_minutes - { pc->time_zone = $1 + time_zone_hhmm (pc, $2, $3); } + { if (! time_zone_hhmm (pc, $2, $3)) YYABORT; + if (INT_ADD_WRAPV (pc->time_zone, $1, &pc->time_zone)) YYABORT; } | tDAYZONE - { pc->time_zone = $1 + 60; } + { pc->time_zone = $1 + 60 * 60; } | tZONE tDST - { pc->time_zone = $1 + 60; } + { pc->time_zone = $1 + 60 * 60; } ; day: @@ -799,8 +802,9 @@ date: if (4 <= $1.digits) { if (pc->parse_datetime_debug) - dbg_printf (_("warning: value %ld has %"PRIuMAX" digits. " \ - "Assuming YYYY/MM/DD\n"), $1.value, $1.digits); + dbg_printf (_("warning: value %"PRIdMAX" has %zu digits. " + "Assuming YYYY/MM/DD\n"), + $1.value, $1.digits); pc->year = $1; pc->month = $3.value; @@ -809,8 +813,9 @@ date: else { if (pc->parse_datetime_debug) - dbg_printf (_("warning: value %ld has less than 4 digits. " \ - "Assuming MM/DD/YY[YY]\n"), $1.value); + dbg_printf (_("warning: value %"PRIdMAX" has less than 4 digits. " + "Assuming MM/DD/YY[YY]\n"), + $1.value); pc->month = $1.value; pc->day = $3.value; @@ -819,18 +824,18 @@ date: } | tUNUMBER tMONTH tSNUMBER { - /* e.g. 17-JUN-1992. */ + /* E.g., 17-JUN-1992. */ pc->day = $1.value; pc->month = $2; - pc->year.value = -$3.value; + if (INT_SUBTRACT_WRAPV (0, $3.value, &pc->year.value)) YYABORT; pc->year.digits = $3.digits; } | tMONTH tSNUMBER tSNUMBER { - /* e.g. JUN-17-1992. */ + /* E.g., JUN-17-1992. */ pc->month = $1; - pc->day = -$2.value; - pc->year.value = -$3.value; + if (INT_SUBTRACT_WRAPV (0, $2.value, &pc->day)) YYABORT; + if (INT_SUBTRACT_WRAPV (0, $3.value, &pc->year.value)) YYABORT; pc->year.digits = $3.digits; } | tMONTH tUNUMBER @@ -863,18 +868,18 @@ iso_8601_date: { /* ISO 8601 format. YYYY-MM-DD. */ pc->year = $1; - pc->month = -$2.value; - pc->day = -$3.value; + if (INT_SUBTRACT_WRAPV (0, $2.value, &pc->month)) YYABORT; + if (INT_SUBTRACT_WRAPV (0, $3.value, &pc->day)) YYABORT; } ; rel: relunit tAGO - { apply_relative_time (pc, $1, $2); } + { if (! apply_relative_time (pc, $1, $2)) YYABORT; } | relunit - { apply_relative_time (pc, $1, 1); } + { if (! apply_relative_time (pc, $1, 1)) YYABORT; } | dayshift - { apply_relative_time (pc, $1, 1); } + { if (! apply_relative_time (pc, $1, 1)) YYABORT; } ; relunit: @@ -891,9 +896,11 @@ relunit: | tMONTH_UNIT { $$ = RELATIVE_TIME_0; $$.month = 1; } | tORDINAL tDAY_UNIT - { $$ = RELATIVE_TIME_0; $$.day = $1 * $2; } + { $$ = RELATIVE_TIME_0; + if (INT_MULTIPLY_WRAPV ($1, $2, &$$.day)) YYABORT; } | tUNUMBER tDAY_UNIT - { $$ = RELATIVE_TIME_0; $$.day = $1.value * $2; } + { $$ = RELATIVE_TIME_0; + if (INT_MULTIPLY_WRAPV ($1.value, $2, &$$.day)) YYABORT; } | tDAY_UNIT { $$ = RELATIVE_TIME_0; $$.day = $1; } | tORDINAL tHOUR_UNIT @@ -927,7 +934,8 @@ relunit_snumber: | tSNUMBER tMONTH_UNIT { $$ = RELATIVE_TIME_0; $$.month = $1.value; } | tSNUMBER tDAY_UNIT - { $$ = RELATIVE_TIME_0; $$.day = $1.value * $2; } + { $$ = RELATIVE_TIME_0; + if (INT_MULTIPLY_WRAPV ($1.value, $2, &$$.day)) YYABORT; } | tSNUMBER tHOUR_UNIT { $$ = RELATIVE_TIME_0; $$.hour = $1.value; } | tSNUMBER tMINUTE_UNIT @@ -946,13 +954,15 @@ seconds: signed_seconds | unsigned_seconds; signed_seconds: tSDECIMAL_NUMBER | tSNUMBER - { $$.tv_sec = $1.value; $$.tv_nsec = 0; } + { if (time_overflow ($1.value)) YYABORT; + $$.tv_sec = $1.value; $$.tv_nsec = 0; } ; unsigned_seconds: tUDECIMAL_NUMBER | tUNUMBER - { $$.tv_sec = $1.value; $$.tv_nsec = 0; } + { if (time_overflow ($1.value)) YYABORT; + $$.tv_sec = $1.value; $$.tv_nsec = 0; } ; number: @@ -966,7 +976,7 @@ hybrid: /* Hybrid all-digit and relative offset, so that we accept e.g., "YYYYMMDD +N days" as well as "YYYYMMDD N days". */ digits_to_date_time (pc, $1); - apply_relative_time (pc, $2, 1); + if (! apply_relative_time (pc, $2, 1)) YYABORT; } ; @@ -1037,7 +1047,7 @@ static table const time_units_table[] = { NULL, 0, 0 } }; -/* Assorted relative-time words. */ +/* Assorted relative-time words. */ static table const relative_time_table[] = { { "TOMORROW", tDAY_SHIFT, 1 }, @@ -1076,7 +1086,7 @@ static table const universal_time_zone_table[] = }; /* The time zone table. This table is necessarily incomplete, as time - zone abbreviations are ambiguous; e.g. Australians interpret "EST" + zone abbreviations are ambiguous; e.g., Australians interpret "EST" as Eastern time in Australia, not as US Eastern Standard Time. You cannot rely on parse_datetime to handle arbitrary time zone abbreviations; use numeric abbreviations like "-0500" instead. */ @@ -1088,8 +1098,8 @@ static table const time_zone_table[] = { "ART", tZONE, -HOUR ( 3) }, /* Argentina */ { "BRT", tZONE, -HOUR ( 3) }, /* Brazil */ { "BRST", tDAYZONE, -HOUR ( 3) }, /* Brazil Summer */ - { "NST", tZONE, -(HOUR ( 3) + 30) }, /* Newfoundland Standard */ - { "NDT", tDAYZONE,-(HOUR ( 3) + 30) }, /* Newfoundland Daylight */ + { "NST", tZONE, -(HOUR ( 3) + 30 * 60) }, /* Newfoundland Standard */ + { "NDT", tDAYZONE,-(HOUR ( 3) + 30 * 60) }, /* Newfoundland Daylight */ { "AST", tZONE, -HOUR ( 4) }, /* Atlantic Standard */ { "ADT", tDAYZONE, -HOUR ( 4) }, /* Atlantic Daylight */ { "CLT", tZONE, -HOUR ( 4) }, /* Chile */ @@ -1122,7 +1132,7 @@ static table const time_zone_table[] = { "EAT", tZONE, HOUR ( 3) }, /* East Africa */ { "MSK", tZONE, HOUR ( 3) }, /* Moscow */ { "MSD", tDAYZONE, HOUR ( 3) }, /* Moscow Daylight */ - { "IST", tZONE, (HOUR ( 5) + 30) }, /* India Standard */ + { "IST", tZONE, (HOUR ( 5) + 30 * 60) }, /* India Standard */ { "SGT", tZONE, HOUR ( 8) }, /* Singapore */ { "KST", tZONE, HOUR ( 9) }, /* Korea Standard */ { "JST", tZONE, HOUR ( 9) }, /* Japan Standard */ @@ -1135,7 +1145,7 @@ static table const time_zone_table[] = /* Military time zone table. Note 'T' is a special case, as it is used as the separator in ISO - 8601 date and time of day representation. */ + 8601 date and time of day representation. */ static table const military_table[] = { { "A", tZONE, -HOUR ( 1) }, @@ -1169,16 +1179,17 @@ static table const military_table[] = /* Convert a time zone expressed as HH:MM into an integer count of - minutes. If MM is negative, then S is of the form HHMM and needs + seconds. If MM is negative, then S is of the form HHMM and needs to be picked apart; otherwise, S is of the form HH. As specified in http://www.opengroup.org/susv3xbd/xbd_chap08.html#tag_08_03, allow only valid TZ range, and consider first two digits as hours, if no - minutes specified. */ + minutes specified. Return true if successful. */ -static long int -time_zone_hhmm (parser_control *pc, textint s, long int mm) +static bool +time_zone_hhmm (parser_control *pc, textint s, intmax_t mm) { - long int n_minutes; + intmax_t n_minutes; + bool overflow = false; /* If the length of S is 1 or 2 and no minutes are specified, interpret it as a number of hours. */ @@ -1188,19 +1199,21 @@ time_zone_hhmm (parser_control *pc, textint s, long int mm) if (mm < 0) n_minutes = (s.value / 100) * 60 + s.value % 100; else - n_minutes = s.value * 60 + (s.negative ? -mm : mm); - - /* If the absolute number of minutes is larger than 24 hours, - arrange to reject it by incrementing pc->zones_seen. Thus, - we allow only values in the range UTC-24:00 to UTC+24:00. */ - if (24 * 60 < abs (n_minutes)) - pc->zones_seen++; + { + overflow |= INT_MULTIPLY_WRAPV (s.value, 60, &n_minutes); + overflow |= (s.negative + ? INT_SUBTRACT_WRAPV (n_minutes, mm, &n_minutes) + : INT_ADD_WRAPV (n_minutes, mm, &n_minutes)); + } - return n_minutes; + if (overflow || ! (-24 * 60 <= n_minutes && n_minutes <= 24 * 60)) + return false; + pc->time_zone = n_minutes * 60; + return true; } static int -to_hour (long int hours, int meridian) +to_hour (intmax_t hours, int meridian) { switch (meridian) { @@ -1214,25 +1227,51 @@ to_hour (long int hours, int meridian) } } -static long int -to_year (textint textyear, bool debug) +enum { TM_YEAR_BASE = 1900 }; +enum { TM_YEAR_BUFSIZE = INT_BUFSIZE_BOUND (int) + 1 }; + +/* Convert TM_YEAR, a year minus 1900, to a string that is numerically + correct even if subtracting 1900 would overflow. */ + +static char const * +tm_year_str (int tm_year, char buf[TM_YEAR_BUFSIZE]) { - long int year = textyear.value; + verify (TM_YEAR_BASE % 100 == 0); + sprintf (buf, &"-%02d%02d"[-TM_YEAR_BASE <= tm_year], + abs (tm_year / 100 + TM_YEAR_BASE / 100), + abs (tm_year % 100)); + return buf; +} - if (year < 0) - year = -year; +/* Convert a text year number to a year minus 1900, working correctly + even if the input is in the range INT_MAX .. INT_MAX + 1900 - 1. */ + +static bool +to_tm_year (textint textyear, bool debug, int *tm_year) +{ + intmax_t year = textyear.value; /* XPG4 suggests that years 00-68 map to 2000-2068, and years 69-99 map to 1969-1999. */ - else if (textyear.digits == 2) + if (0 <= year && textyear.digits == 2) { year += year < 69 ? 2000 : 1900; if (debug) - dbg_printf (_("warning: adjusting year value %ld to %ld\n"), + dbg_printf (_("warning: adjusting year value %"PRIdMAX + " to %"PRIdMAX"\n"), textyear.value, year); } - return year; + if (year < 0 + ? INT_SUBTRACT_WRAPV (-TM_YEAR_BASE, year, tm_year) + : INT_SUBTRACT_WRAPV (year, TM_YEAR_BASE, tm_year)) + { + if (debug) + dbg_printf (_("error: out-of-range year %"PRIdMAX"\n"), year); + return false; + } + + return true; } static table const * _GL_ATTRIBUTE_PURE @@ -1261,12 +1300,13 @@ lookup_zone (parser_control const *pc, char const *name) /* Yield the difference between *A and *B, measured in seconds, ignoring leap seconds. The body of this function is taken directly from the GNU C Library; - see src/strftime.c. */ -static long int -tm_diff (struct tm const *a, struct tm const *b) + see strftime.c. */ +static int +tm_diff (const struct tm *a, const struct tm *b) { /* Compute intervening leap days correctly even if year is negative. - Take care to avoid int overflow in leap day calculations. */ + Take care to avoid int overflow in leap day calculations, + but it's OK to assume that A and B are close to each other. */ int a4 = SHR (a->tm_year, 2) + SHR (TM_YEAR_BASE, 2) - ! (a->tm_year & 3); int b4 = SHR (b->tm_year, 2) + SHR (TM_YEAR_BASE, 2) - ! (b->tm_year & 3); int a100 = a4 / 25 - (a4 % 25 < 0); @@ -1274,10 +1314,9 @@ tm_diff (struct tm const *a, struct tm const *b) int a400 = SHR (a100, 2); int b400 = SHR (b100, 2); int intervening_leap_days = (a4 - b4) - (a100 - b100) + (a400 - b400); - long int ayear = a->tm_year; - long int years = ayear - b->tm_year; - long int days = (365 * years + intervening_leap_days - + (a->tm_yday - b->tm_yday)); + int years = a->tm_year - b->tm_year; + int days = (365 * years + intervening_leap_days + + (a->tm_yday - b->tm_yday)); return (60 * (60 * (24 * days + (a->tm_hour - b->tm_hour)) + (a->tm_min - b->tm_min)) + (a->tm_sec - b->tm_sec)); @@ -1296,16 +1335,13 @@ lookup_word (parser_control const *pc, char *word) /* Make it uppercase. */ for (p = word; *p; p++) - { - unsigned char ch = *p; - *p = c_toupper (ch); - } + *p = c_toupper (to_uchar (*p)); for (tp = meridian_table; tp->name; tp++) if (strcmp (word, tp->name) == 0) return tp; - /* See if we have an abbreviation for a month. */ + /* See if we have an abbreviation for a month. */ wordlen = strlen (word); abbrev = wordlen == 3 || (wordlen == 4 && word[3] == '.'); @@ -1323,7 +1359,7 @@ lookup_word (parser_control const *pc, char *word) if (strcmp (word, tp->name) == 0) return tp; - /* Strip off any plural and try the units table again. */ + /* Strip off any plural and try the units table again. */ if (word[wordlen - 1] == 'S') { word[wordlen - 1] = '\0'; @@ -1337,13 +1373,13 @@ lookup_word (parser_control const *pc, char *word) if (strcmp (word, tp->name) == 0) return tp; - /* Military time zones. */ + /* Military time zones. */ if (wordlen == 1) for (tp = military_table; tp->name; tp++) if (word[0] == tp->name[0]) return tp; - /* Drop out any periods and try the time zone table again. */ + /* Drop out any periods and try the time zone table again. */ for (period_found = false, p = q = word; (*p = *q); q++) if (*q == '.') period_found = true; @@ -1366,59 +1402,43 @@ yylex (union YYSTYPE *lvalp, parser_control *pc) while (c = *pc->input, c_isspace (c)) pc->input++; - if (ISDIGIT (c) || c == '-' || c == '+') + if (c_isdigit (c) || c == '-' || c == '+') { char const *p; int sign; - unsigned long int value; + intmax_t value = 0; if (c == '-' || c == '+') { sign = c == '-' ? -1 : 1; while (c = *++pc->input, c_isspace (c)) continue; - if (! ISDIGIT (c)) + if (! c_isdigit (c)) /* skip the '-' sign */ continue; } else sign = 0; p = pc->input; - for (value = 0; ; value *= 10) + + do { - unsigned long int value1 = value + (c - '0'); - if (value1 < value) + if (INT_MULTIPLY_WRAPV (value, 10, &value)) return '?'; - value = value1; - c = *++p; - if (! ISDIGIT (c)) - break; - if (ULONG_MAX / 10 < value) + if (INT_ADD_WRAPV (value, sign < 0 ? '0' - c : c - '0', &value)) return '?'; + c = *++p; } - if ((c == '.' || c == ',') && ISDIGIT (p[1])) + while (c_isdigit (c)); + + if ((c == '.' || c == ',') && c_isdigit (p[1])) { time_t s; int ns; int digits; - unsigned long int value1; - /* Check for overflow when converting value to time_t. */ - if (sign < 0) - { - s = - value; - if (0 < s) - return '?'; - value1 = -s; - } - else - { - s = value; - if (s < 0) - return '?'; - value1 = s; - } - if (value != value1) + if (time_overflow (value)) return '?'; + s = value; /* Accumulate fraction, to ns precision. */ p++; @@ -1426,19 +1446,19 @@ yylex (union YYSTYPE *lvalp, parser_control *pc) for (digits = 2; digits <= LOG10_BILLION; digits++) { ns *= 10; - if (ISDIGIT (*p)) + if (c_isdigit (*p)) ns += *p++ - '0'; } /* Skip excess digits, truncating toward -Infinity. */ if (sign < 0) - for (; ISDIGIT (*p); p++) + for (; c_isdigit (*p); p++) if (*p != '0') { ns++; break; } - while (ISDIGIT (*p)) + while (c_isdigit (*p)) p++; /* Adjust to the timespec convention, which is that @@ -1446,9 +1466,9 @@ yylex (union YYSTYPE *lvalp, parser_control *pc) negative. */ if (sign < 0 && ns) { - s--; - if (! (s < 0)) + if (s == TYPE_MINIMUM (time_t)) return '?'; + s--; ns = BILLION - ns; } @@ -1460,18 +1480,7 @@ yylex (union YYSTYPE *lvalp, parser_control *pc) else { lvalp->textintval.negative = sign < 0; - if (sign < 0) - { - lvalp->textintval.value = - value; - if (0 < lvalp->textintval.value) - return '?'; - } - else - { - lvalp->textintval.value = value; - if (lvalp->textintval.value < 0) - return '?'; - } + lvalp->textintval.value = value; lvalp->textintval.digits = p - pc->input; pc->input = p; return sign ? tSNUMBER : tUNUMBER; @@ -1557,28 +1566,20 @@ mktime_ok (timezone_t tz, struct tm const *tm0, struct tm const *tm1, time_t t) | (tm0->tm_year ^ tm1->tm_year)); } -/* A reasonable upper bound for the size of ordinary TZ strings. - Use heap allocation if TZ's length exceeds this. */ -enum { TZBUFSIZE = 100 }; - -/* A reasonable upper bound for the buffer used in debug print outs. - see days_to_name(), debug_strftime() and debug_mktime_not_ok() */ -enum { DBGBUFSIZE = 100 }; - -/* debugging: format a 'struct tm' into a buffer, taking the parser's - timezone information into account (if pc!=NULL). */ -static const char* -debug_strfdatetime (const struct tm *tm, const parser_control *pc, - char* /*output*/ buf, size_t n) +/* Debugging: format a 'struct tm' into a buffer, taking the parser's + timezone information into account (if pc != NULL). */ +static char const * +debug_strfdatetime (struct tm const *tm, parser_control const *pc, + char *buf, size_t n) { /* TODO: 1. find an optimal way to print date string in a clear and unambiguous - format. Currently, always add '(Y-M-D)' prefix. + format. Currently, always add '(Y-M-D)' prefix. Consider '2016y01m10d' or 'year(2016) month(01) day(10)'. If the user needs debug printing, it means he/she already having issues with the parsing - better to avoid formats that could - be mis-interpreted (e.g. just YYYY-MM-DD). + be mis-interpreted (e.g., just YYYY-MM-DD). 2. Can strftime be used instead? depends if it is portable and can print invalid dates on all systems. @@ -1590,46 +1591,47 @@ debug_strfdatetime (const struct tm *tm, const parser_control *pc, 5. Print nanosecond information ? NOTE: - Printed date/time values might not be valid, e.g. '2016-02-31' + Printed date/time values might not be valid, e.g., '2016-02-31' or '2016-19-2016' . These are the values as parsed from the user string, before validation. */ int m = nstrftime (buf, n, "(Y-M-D) %Y-%m-%d %H:%M:%S", tm, 0, 0); - /* if parser_control information was provided (for timezone), - and there's enough space in the buffer - add timezone info */ - if (pc != NULL && ((n-m)>0)) + /* If parser_control information was provided (for timezone), + and there's enough space in the buffer, add timezone info. */ + if (pc && m < n && pc->zones_seen) { - long int tz = (pc->zones_seen) - ? pc->time_zone - : pc->debug_default_input_timezone; + int tz = pc->time_zone; - /* Account for DST if tLOCAL_ZONE was seen */ - if (pc->local_zones_seen && !pc->zones_seen && pc->local_isdst==1) - tz += 60; + /* Account for DST if tLOCAL_ZONE was seen. */ + if (pc->local_zones_seen && !pc->zones_seen && 0 < pc->local_isdst) + tz += 60 * 60; - snprintf (&buf[m],n-m," TZ=%+03d:%02d", (int)(tz/60), abs ((int)tz)%60); + char time_zone_buf[TIME_ZONE_BUFSIZE]; + snprintf (&buf[m], n - m, " TZ=%s", time_zone_str (tz, time_zone_buf)); } return buf; } -static const char* -debug_strfdate (const struct tm *tm, char* /*output*/ buf, size_t n) +static char const * +debug_strfdate (struct tm const *tm, char *buf, size_t n) { - snprintf (buf,n,"(Y-M-D) %04d-%02d-%02d", - tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday); + char tm_year_buf[TM_YEAR_BUFSIZE]; + snprintf (buf, n, "(Y-M-D) %s-%02d-%02d", + tm_year_str (tm->tm_year, tm_year_buf), + tm->tm_mon + 1, tm->tm_mday); return buf; } -static const char* -debug_strftime (const struct tm *tm, char* /*output*/ buf, size_t n) +static char const * +debug_strftime (struct tm const *tm, char *buf, size_t n) { - snprintf (buf,n,"%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec); + snprintf (buf, n, "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec); return buf; } -/* If 'mktime_ok()' failed, display the failed time values, - and provide possible hints. Example output: +/* If mktime_ok failed, display the failed time values, + and provide possible hints. Example output: date: error: invalid date/time value: date: user provided time: '(Y-M-D) 2006-04-02 02:45:00' @@ -1642,9 +1644,9 @@ debug_strftime (const struct tm *tm, char* /*output*/ buf, size_t n) */ static void debug_mktime_not_ok (struct tm const *tm0, struct tm const *tm1, - const parser_control *pc, bool time_zone_seen) + parser_control const *pc, bool time_zone_seen) { - /* TODO: handle t==-1 (as in 'mktime_ok') */ + /* TODO: handle t == -1 (as in 'mktime_ok'). */ char tmp[DBGBUFSIZE]; int i; const bool eq_sec = (tm0->tm_sec == tm1->tm_sec); @@ -1662,19 +1664,25 @@ debug_mktime_not_ok (struct tm const *tm0, struct tm const *tm1, dbg_printf (_("error: invalid date/time value:\n")); dbg_printf (_(" user provided time: '%s'\n"), - debug_strfdatetime (tm0, pc, tmp, sizeof (tmp))); + debug_strfdatetime (tm0, pc, tmp, sizeof tmp)); dbg_printf (_(" normalized time: '%s'\n"), - debug_strfdatetime (tm1, pc, tmp, sizeof (tmp))); - /* NOTEs: the format must be aligned with debug_strfdatetime() and the two - DEBUG statements above. this string is not translated. */ - i = snprintf (tmp, sizeof(tmp), + debug_strfdatetime (tm1, pc, tmp, sizeof tmp)); + /* The format must be aligned with debug_strfdatetime and the two + DEBUG statements above. This string is not translated. */ + i = snprintf (tmp, sizeof tmp, " %4s %2s %2s %2s %2s %2s", - eq_year?"":"----", eq_month?"":"--", eq_mday?"":"--", - eq_hour?"":"--", eq_min?"":"--", eq_sec?"":"--"); - /* Trim trailing whitespace */ - if ((i>0) && (i0) && (tmp[i-1]==' ')) + if (sizeof tmp - 1 < i) + i = sizeof tmp - 1; + while (0 < i && tmp[i - 1] == ' ') --i; tmp[i] = '\0'; } @@ -1686,20 +1694,8 @@ debug_mktime_not_ok (struct tm const *tm0, struct tm const *tm1, if (!eq_mday && !eq_month) dbg_printf (_(" invalid day/month combination;\n")); dbg_printf (_(" numeric values overflow;\n")); - dbg_printf (" %s\n",time_zone_seen?_("incorrect timezone") - :_("missing timezone")); -} - - -/* Returns the effective local timezone, in minutes. */ -static long int -get_effective_timezone (timezone_t tz) -{ - time_t z = 0; - struct tm tm; - if (! localtime_rz (tz, &z, &tm)) - return 0; - return timegm (&tm) / 60; + dbg_printf (" %s\n", (time_zone_seen ? _("incorrect timezone") + : _("missing timezone"))); } /* The original interface: run with debug=false and the default timezone. */ @@ -1726,34 +1722,35 @@ parse_datetime2 (struct timespec *result, char const *p, struct timespec const *now, unsigned int flags, timezone_t tzdefault, char const *tzstring) { - time_t Start; - long int Start_ns; - struct tm tmp; struct tm tm; struct tm tm0; - parser_control pc; - struct timespec gettime_buffer; - unsigned char c; - timezone_t tz = tzdefault; - bool ok = true; - char dbg_ord[DBGBUFSIZE]; + char time_zone_buf[TIME_ZONE_BUFSIZE]; char dbg_tm[DBGBUFSIZE]; + bool ok = false; char const *input_sentinel = p + strlen (p); char *tz1alloc = NULL; + + /* A reasonable upper bound for the size of ordinary TZ strings. + Use heap allocation if TZ's length exceeds this. */ + enum { TZBUFSIZE = 100 }; char tz1buf[TZBUFSIZE]; + struct timespec gettime_buffer; if (! now) { gettime (&gettime_buffer); now = &gettime_buffer; } - Start = now->tv_sec; - Start_ns = now->tv_nsec; + time_t Start = now->tv_sec; + int Start_ns = now->tv_nsec; + unsigned char c; while (c = *p, c_isspace (c)) p++; + timezone_t tz = tzdefault; + if (strncmp (p, "TZ=\"", 4) == 0) { char const *tzbase = p + 4; @@ -1797,6 +1794,7 @@ parse_datetime2 (struct timespec *result, char const *p, } } + struct tm tmp; if (! localtime_rz (tz, &now->tv_sec, &tmp)) return false; @@ -1806,9 +1804,14 @@ parse_datetime2 (struct timespec *result, char const *p, if (*p == '\0') p = "0"; + parser_control pc; pc.input = p; - pc.year.value = tmp.tm_year; - pc.year.value += TM_YEAR_BASE; + if (INT_ADD_WRAPV (tmp.tm_year, TM_YEAR_BASE, &pc.year.value)) + { + if (pc.parse_datetime_debug) + dbg_printf (_("error: initial year out of range\n")); + goto fail; + } pc.year.digits = 0; pc.month = tmp.tm_mon + 1; pc.day = tmp.tm_mday; @@ -1828,17 +1831,15 @@ parse_datetime2 (struct timespec *result, char const *p, pc.local_zones_seen = 0; pc.dsts_seen = 0; pc.zones_seen = 0; - pc.year_seen = 0; - pc.parse_datetime_debug = (flags & PARSE_DATETIME_DEBUG)!=0; - pc.debug_dates_seen = 0; - pc.debug_days_seen = 0; - pc.debug_times_seen = 0; - pc.debug_local_zones_seen = 0; - pc.debug_dsts_seen = 0; - pc.debug_zones_seen = 0; - pc.debug_year_seen = 0; + pc.year_seen = false; + pc.parse_datetime_debug = (flags & PARSE_DATETIME_DEBUG) != 0; + pc.debug_dates_seen = false; + pc.debug_days_seen = false; + pc.debug_times_seen = false; + pc.debug_local_zones_seen = false; + pc.debug_zones_seen = false; + pc.debug_year_seen = false; pc.debug_ordinal_day_seen = false; - pc.debug_default_input_timezone = 0; #if HAVE_STRUCT_TM_TM_ZONE pc.local_time_zone_table[0].name = tmp.tm_zone; @@ -1852,7 +1853,11 @@ parse_datetime2 (struct timespec *result, char const *p, int quarter; for (quarter = 1; quarter <= 3; quarter++) { - time_t probe = Start + quarter * (90 * 24 * 60 * 60); + intmax_t iprobe; + if (INT_ADD_WRAPV (Start, quarter * (90 * 24 * 60 * 60), &iprobe) + || time_overflow (iprobe)) + break; + time_t probe = iprobe; struct tm probe_tm; if (localtime_rz (tz, &probe, &probe_tm) && probe_tm.tm_zone && probe_tm.tm_isdst != pc.local_time_zone_table[0].value) @@ -1898,80 +1903,52 @@ parse_datetime2 (struct timespec *result, char const *p, pc.local_time_zone_table[1].name = NULL; } - pc.debug_default_input_timezone = get_effective_timezone (tz); - if (yyparse (&pc) != 0) { if (pc.parse_datetime_debug) - { - if (input_sentinel <= pc.input) - dbg_printf (_("error: parsing failed\n"), pc.input); - else - { - dbg_printf (_("error: parsing failed, stopped at '%s'\n"), - pc.input); - } - } + dbg_printf ((input_sentinel <= pc.input + ? _("error: parsing failed\n") + : _("error: parsing failed, stopped at '%s'\n")), + pc.input); goto fail; } - /* determine effective timezone source */ + + /* Determine effective timezone source. */ + if (pc.parse_datetime_debug) { - long int time_zone = pc.debug_default_input_timezone; + dbg_printf (_("input timezone: ")); if (pc.timespec_seen) - { - time_zone = 0; - strncpy (dbg_tm, _("'@timespec' - always UTC0"), sizeof (dbg_tm)-1); - } + fprintf (stderr, _("'@timespec' - always UTC")); else if (pc.zones_seen) - { - time_zone = pc.time_zone; - strncpy (dbg_tm, _("parsed date/time string"), sizeof (dbg_tm)-1); - } + fprintf (stderr, _("parsed date/time string")); else if (tzstring) { if (tz != tzdefault) - { - snprintf (dbg_tm, sizeof(dbg_tm), _("TZ=\"%s\" in date string"), - tzstring); - } + fprintf (stderr, _("TZ=\"%s\" in date string"), tzstring); else if (STREQ (tzstring, "UTC0")) { - /* Special case: using 'date -u' simply set TZ=UTC0 */ - strncpy (dbg_tm, _("TZ=UTC0 environment value or -u"), - sizeof (dbg_tm)-1); + /* Special case: 'date -u' sets TZ="UTC0". */ + fprintf (stderr, _("TZ=\"UTC0\" environment value or -u")); } else - { - snprintf (dbg_tm, sizeof(dbg_tm), - _("TZ=\"%s\" environment value"), tzstring); - } + fprintf (stderr, _("TZ=\"%s\" environment value"), tzstring); } else - { - strncpy (dbg_tm, _("system default"), sizeof (dbg_tm)-1); - } - - /* Ensure it's NUL terminated after strncpy */ - dbg_tm[sizeof (dbg_tm)-1] = '\0'; + fprintf (stderr, _("system default")); /* Account for DST changes if tLOCAL_ZONE was seen. local timezone only changes DST and is relative to the default timezone.*/ - if (pc.local_zones_seen && !pc.zones_seen && pc.local_isdst==1) - { - time_zone += 60; - strncat (dbg_tm, ", dst", - sizeof (dbg_tm) - strlen (dbg_tm) - 1); - } + if (pc.local_zones_seen && !pc.zones_seen && 0 < pc.local_isdst) + fprintf (stderr, ", dst"); - if (pc.parse_datetime_debug) - dbg_printf (_("input timezone: %+03d:%02d (set from %s)\n"), - (int) (time_zone / 60), abs ((int) (time_zone % 60)), - dbg_tm); + if (pc.zones_seen) + fprintf (stderr, " (%s)", time_zone_str (pc.time_zone, time_zone_buf)); + fputc ('\n', stderr); } if (pc.timespec_seen) @@ -1991,33 +1968,39 @@ parse_datetime2 (struct timespec *result, char const *p, dbg_printf ("error: seen multiple days parts\n"); if (pc.dsts_seen > 1) dbg_printf ("error: seen multiple daylight-saving parts\n"); - if ( (pc.local_zones_seen + pc.zones_seen) > 1) + if ((pc.local_zones_seen + pc.zones_seen) > 1) dbg_printf ("error: seen multiple time-zone parts\n"); } goto fail; } - tm.tm_year = to_year (pc.year, pc.parse_datetime_debug) - TM_YEAR_BASE; - tm.tm_mon = pc.month - 1; - tm.tm_mday = pc.day; + if (! to_tm_year (pc.year, pc.parse_datetime_debug, &tm.tm_year) + || INT_ADD_WRAPV (pc.month, -1, &tm.tm_mon) + || INT_ADD_WRAPV (pc.day, 0, &tm.tm_mday)) + { + if (pc.parse_datetime_debug) + dbg_printf (_("error: year, month, or day overflow\n")); + goto fail; + } if (pc.times_seen || (pc.rels_seen && ! pc.dates_seen && ! pc.days_seen)) { tm.tm_hour = to_hour (pc.hour, pc.meridian); if (tm.tm_hour < 0) { - const char* mrd = (pc.meridian==MERam)?"am": - (pc.meridian==MERpm)?"pm":""; + char const *mrd = (pc.meridian == MERam ? "am" + : pc.meridian == MERpm ?"pm" : ""); if (pc.parse_datetime_debug) - dbg_printf (_("error: invalid hour %ld%s\n"), pc.hour, mrd); - + dbg_printf (_("error: invalid hour %"PRIdMAX"%s\n"), + pc.hour, mrd); goto fail; } tm.tm_min = pc.minutes; tm.tm_sec = pc.seconds.tv_sec; if (pc.parse_datetime_debug) - dbg_printf (_("using %s time as starting value: '%s'\n"), - (pc.times_seen)?_("specified"):_("current"), - debug_strftime (&tm,dbg_tm,sizeof (dbg_tm))); + dbg_printf ((pc.times_seen + ? _("using specified time as starting value: '%s'\n") + : _("using current time as starting value: '%s'\n")), + debug_strftime (&tm, dbg_tm, sizeof dbg_tm)); } else { @@ -2045,7 +2028,6 @@ parse_datetime2 (struct timespec *result, char const *p, if (! pc.zones_seen) { debug_mktime_not_ok (&tm0, &tm, &pc, pc.zones_seen); - goto fail; } else @@ -2060,60 +2042,64 @@ parse_datetime2 (struct timespec *result, char const *p, problem, set the time zone to 1 hour behind UTC temporarily by setting TZ="XXX1:00" and try mktime again. */ - long int time_zone = pc.time_zone; - long int abs_time_zone = time_zone < 0 ? - time_zone : time_zone; - long int abs_time_zone_hour = abs_time_zone / 60; - int abs_time_zone_min = abs_time_zone % 60; - char tz2buf[sizeof "XXX+0:00" + TYPE_WIDTH (pc.time_zone) / 3]; - timezone_t tz2; - sprintf (tz2buf, "XXX%s%ld:%02d", &"-"[time_zone < 0], - abs_time_zone_hour, abs_time_zone_min); - tz2 = tzalloc (tz2buf); + char tz2buf[sizeof "XXX" - 1 + TIME_ZONE_BUFSIZE]; + tz2buf[0] = tz2buf[1] = tz2buf[2] = 'X'; + time_zone_str (pc.time_zone, &tz2buf[3]); + timezone_t tz2 = tzalloc (tz2buf); if (!tz2) { if (pc.parse_datetime_debug) dbg_printf (_("error: tzalloc (\"%s\") failed\n"), tz2buf); - goto fail; } tm = tm0; Start = mktime_z (tz2, &tm); - ok = mktime_ok (tz2, &tm0, &tm, Start); + bool mktime_failed = !mktime_ok (tz2, &tm0, &tm, Start); tzfree (tz2); - if (! ok) + if (mktime_failed) { debug_mktime_not_ok (&tm0, &tm, &pc, pc.zones_seen); - - goto done; + goto fail; } } } + char dbg_ord[DBGBUFSIZE]; + if (pc.days_seen && ! pc.dates_seen) { - tm.tm_mday += ((pc.day_number - tm.tm_wday + 7) % 7 - + 7 * (pc.day_ordinal - - (0 < pc.day_ordinal - && tm.tm_wday != pc.day_number))); - tm.tm_isdst = -1; - Start = mktime_z (tz, &tm); + intmax_t dayincr; + if (INT_MULTIPLY_WRAPV ((pc.day_ordinal + - (0 < pc.day_ordinal + && tm.tm_wday != pc.day_number)), + 7, &dayincr) + || INT_ADD_WRAPV ((pc.day_number - tm.tm_wday + 7) % 7, + dayincr, &dayincr) + || INT_ADD_WRAPV (dayincr, tm.tm_mday, &tm.tm_mday)) + Start = -1; + else + { + tm.tm_isdst = -1; + Start = mktime_z (tz, &tm); + } + if (Start == (time_t) -1) { if (pc.parse_datetime_debug) - dbg_printf (_("error: day '%s' (day ordinal=%ld number=%d) " \ + dbg_printf (_("error: day '%s' " + "(day ordinal=%"PRIdMAX" number=%d) " "resulted in an invalid date: '%s'\n"), - str_days (&pc,dbg_ord,sizeof (dbg_ord)), - pc.day_ordinal,pc.day_number, + str_days (&pc, dbg_ord, sizeof dbg_ord), + pc.day_ordinal, pc.day_number, debug_strfdatetime (&tm, &pc, dbg_tm, - sizeof (dbg_tm))); - + sizeof dbg_tm)); goto fail; } if (pc.parse_datetime_debug) dbg_printf (_("new start date: '%s' is '%s'\n"), - str_days (&pc,dbg_ord,sizeof (dbg_ord)), - debug_strfdatetime (&tm, &pc, dbg_tm,sizeof (dbg_tm))); + str_days (&pc, dbg_ord, sizeof dbg_ord), + debug_strfdatetime (&tm, &pc, dbg_tm, sizeof dbg_tm)); } @@ -2121,15 +2107,15 @@ parse_datetime2 (struct timespec *result, char const *p, { if (!pc.dates_seen && !pc.days_seen) dbg_printf (_("using current date as starting value: '%s'\n"), - debug_strfdate (&tm,dbg_tm,sizeof (dbg_tm))); + debug_strfdate (&tm, dbg_tm, sizeof dbg_tm)); if (pc.days_seen && pc.dates_seen) - dbg_printf (_("warning: day (%s) ignored when explicit dates " \ + dbg_printf (_("warning: day (%s) ignored when explicit dates " "are given\n"), - str_days (&pc,dbg_ord,sizeof (dbg_ord))); + str_days (&pc, dbg_ord, sizeof dbg_ord)); dbg_printf (_("starting date/time: '%s'\n"), - debug_strfdatetime (&tm, &pc, dbg_tm,sizeof (dbg_tm))); + debug_strfdatetime (&tm, &pc, dbg_tm, sizeof dbg_tm)); } /* Add relative date. */ @@ -2137,27 +2123,23 @@ parse_datetime2 (struct timespec *result, char const *p, { if (pc.parse_datetime_debug) { - if ((pc.rel.year != 0 || pc.rel.month !=0) && tm.tm_mday!=15) - dbg_printf (_("warning: when adding relative months/years, " \ - "it is recommended to specify the 15th of the " \ + if ((pc.rel.year != 0 || pc.rel.month != 0) && tm.tm_mday != 15) + dbg_printf (_("warning: when adding relative months/years, " + "it is recommended to specify the 15th of the " "months\n")); - if (pc.rel.day != 0 && tm.tm_hour!=12) - dbg_printf (_("warning: when adding relative days, " \ - "it is recommended to specify 12:00pm\n")); + if (pc.rel.day != 0 && tm.tm_hour != 12) + dbg_printf (_("warning: when adding relative days, " + "it is recommended to specify noon\n")); } - int year = tm.tm_year + pc.rel.year; - int month = tm.tm_mon + pc.rel.month; - int day = tm.tm_mday + pc.rel.day; - if (((year < tm.tm_year) ^ (pc.rel.year < 0)) - | ((month < tm.tm_mon) ^ (pc.rel.month < 0)) - | ((day < tm.tm_mday) ^ (pc.rel.day < 0))) + int year, month, day; + if (INT_ADD_WRAPV (tm.tm_year, pc.rel.year, &year) + || INT_ADD_WRAPV (tm.tm_mon, pc.rel.month, &month) + || INT_ADD_WRAPV (tm.tm_mday, pc.rel.day, &day)) { - /* TODO: what is the actual error? int-value wrap-around? */ if (pc.parse_datetime_debug) - dbg_printf (_("error: %s:%d\n"), __FILE__,__LINE__); - + dbg_printf (_("error: %s:%d\n"), __FILE__, __LINE__); goto fail; } tm.tm_year = year; @@ -2171,42 +2153,42 @@ parse_datetime2 (struct timespec *result, char const *p, if (Start == (time_t) -1) { if (pc.parse_datetime_debug) - dbg_printf(_("error: adding relative date resulted " \ - "in an invalid date: '%s'\n"), - debug_strfdatetime (&tm, &pc, dbg_tm, - sizeof (dbg_tm))); - + dbg_printf (_("error: adding relative date resulted " + "in an invalid date: '%s'\n"), + debug_strfdatetime (&tm, &pc, dbg_tm, + sizeof dbg_tm)); goto fail; } if (pc.parse_datetime_debug) { - dbg_printf (_("after date adjustment " \ - "(%+ld years, %+ld months, %+ld days),\n"), + dbg_printf (_("after date adjustment " + "(%+"PRIdMAX" years, %+"PRIdMAX" months, " + "%+"PRIdMAX" days),\n"), pc.rel.year, pc.rel.month, pc.rel.day); dbg_printf (_(" new date/time = '%s'\n"), debug_strfdatetime (&tm, &pc, dbg_tm, - sizeof (dbg_tm))); + sizeof dbg_tm)); - /* warn about crossing DST due to time adjustment. + /* Warn about crossing DST due to time adjustment. Example: https://bugs.gnu.org/8357 env TZ=Europe/Helsinki \ date --debug \ -d 'Mon Mar 28 00:36:07 2011 EEST 1 day ago' This case is different than DST changes due to time adjustment, - i.e. "1 day ago" vs "24 hours ago" are calculated in different + i.e., "1 day ago" vs "24 hours ago" are calculated in different places. 'tm0.tm_isdst' contains the DST of the input date, 'tm.tm_isdst' is the normalized result after calling - mktime(&tm). + mktime (&tm). */ - if ((tm0.tm_isdst!=-1) && (tm.tm_isdst != tm0.tm_isdst)) - dbg_printf (_("warning: daylight saving time changed after " \ + if (tm0.tm_isdst != -1 && tm.tm_isdst != tm0.tm_isdst) + dbg_printf (_("warning: daylight saving time changed after " "date adjustment\n")); - /* warn if the user did not ask to adjust days but mday changed, + /* Warn if the user did not ask to adjust days but mday changed, or user did not ask to adjust months/days but the month changed. @@ -2218,16 +2200,18 @@ parse_datetime2 (struct timespec *result, char const *p, 2016-02-29 + 1 year => 2017-02-29 => 2017-03-01. User asked to adjust year, but the month changed from 02 to 03. */ - if (((pc.rel.day==0) && (tm.tm_mday != day)) - || ((pc.rel.day==0) && (pc.rel.month==0) - && (tm.tm_mon != month))) + if (pc.rel.day == 0 + && (tm.tm_mday != day + || (pc.rel.month == 0 && tm.tm_mon != month))) { - dbg_printf (_("warning: month/year adjustment resulted in "\ + dbg_printf (_("warning: month/year adjustment resulted in " "shifted dates:\n")); - dbg_printf (_(" adjusted Y M D: %04d %02d %02d\n"), - year+1900, month+1, day); - dbg_printf (_(" normalized Y M D: %04d %02d %02d\n"), - tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday); + char tm_year_buf[TM_YEAR_BUFSIZE]; + dbg_printf (_(" adjusted Y M D: %s %02d %02d\n"), + tm_year_str (year, tm_year_buf), month + 1, day); + dbg_printf (_(" normalized Y M D: %s %02d %02d\n"), + tm_year_str (tm.tm_year, tm_year_buf), + tm.tm_mon + 1, tm.tm_mday); } } @@ -2237,39 +2221,37 @@ parse_datetime2 (struct timespec *result, char const *p, so this block must follow others that clobber Start. */ if (pc.zones_seen) { - long int delta = pc.time_zone * 60; - time_t t1; + intmax_t delta = pc.time_zone, t1; + bool overflow = false; #ifdef HAVE_TM_GMTOFF - delta -= tm.tm_gmtoff; + long int utcoff = tm.tm_gmtoff; #else time_t t = Start; struct tm gmt; - if (! gmtime_r (&t, &gmt)) - { - /* TODO: use 'warn(3)' + print errno ? */ - if (pc.parse_datetime_debug) - dbg_printf (_("error: gmtime failed for t=%ld\n"),t); - - goto fail; - } - delta -= tm_diff (&tm, &gmt); + int utcoff = (gmtime_r (&t, &gmt) + ? tm_diff (&tm, &gmt) + : (overflow = true, 0)); #endif - t1 = Start - delta; - if ((Start < t1) != (delta < 0)) + overflow |= INT_SUBTRACT_WRAPV (delta, utcoff, &delta); + overflow |= INT_SUBTRACT_WRAPV (Start, delta, &t1); + if (overflow || time_overflow (t1)) { if (pc.parse_datetime_debug) - dbg_printf (_("error: timezone %ld caused time_t overflow\n"), + dbg_printf (_("error: timezone %d caused time_t overflow\n"), pc.time_zone); - - goto fail; /* time_t overflow */ + goto fail; } Start = t1; } if (pc.parse_datetime_debug) - dbg_printf (_("'%s' = %ld epoch-seconds\n"), - debug_strfdatetime (&tm, &pc, dbg_tm, sizeof (dbg_tm)), - Start); + { + intmax_t Starti = Start; + dbg_printf (_("'%s' = %"PRIdMAX" epoch-seconds\n"), + debug_strfdatetime (&tm, &pc, dbg_tm, sizeof dbg_tm), + Starti); + } + /* Add relative hours, minutes, and seconds. On hosts that support leap seconds, ignore the possibility of leap seconds; e.g., @@ -2279,116 +2261,103 @@ parse_datetime2 (struct timespec *result, char const *p, must be applied before relative times, and if mktime is applied again the time zone will be lost. */ { - long int sum_ns = pc.seconds.tv_nsec + pc.rel.ns; - long int normalized_ns = (sum_ns % BILLION + BILLION) % BILLION; - time_t t0 = Start; - long int d1 = 60 * 60 * pc.rel.hour; - time_t t1 = t0 + d1; - long int d2 = 60 * pc.rel.minutes; - time_t t2 = t1 + d2; - long_time_t d3 = pc.rel.seconds; - long_time_t t3 = t2 + d3; - long int d4 = (sum_ns - normalized_ns) / BILLION; - long_time_t t4 = t3 + d4; - time_t t5 = t4; - - if ((d1 / (60 * 60) ^ pc.rel.hour) - | (d2 / 60 ^ pc.rel.minutes) - | ((t1 < t0) ^ (d1 < 0)) - | ((t2 < t1) ^ (d2 < 0)) - | ((t3 < t2) ^ (d3 < 0)) - | ((t4 < t3) ^ (d4 < 0)) - | (t5 != t4)) - { - if (pc.parse_datetime_debug) - dbg_printf (_("error: adding relative time caused an " \ - "overflow\n")); - - goto fail; - } - - if (pc.parse_datetime_debug - && (pc.rel.hour | pc.rel.minutes | pc.rel.seconds | pc.rel.ns)) - { - struct tm lmt; - dbg_printf (_("after time adjustment (%+ld hours, " \ - "%+ld minutes, %+ld seconds, %+ld ns),\n"), - pc.rel.hour,pc.rel.minutes,pc.rel.seconds,pc.rel.ns); - dbg_printf (_(" new time = %ld epoch-seconds\n"),t5); - - /* warn about crossing DST due to time adjustment. - Example: https://bugs.gnu.org/8357 - env TZ=Europe/Helsinki \ - date --debug \ - -d 'Mon Mar 28 00:36:07 2011 EEST 24 hours ago' - - This case is different than DST changes due to days adjustment, - i.e. "1 day ago" vs "24 hours ago" are calculated in different - places. - - 'tm.tm_isdst' contains the date after date adjustment. - */ - if (tm.tm_isdst != -1 && localtime_rz (tz, &t5, &lmt) - && tm.tm_isdst != lmt.tm_isdst) - dbg_printf (_("warning: daylight saving time changed after " \ - "time adjustment\n")); - } + intmax_t orig_ns = pc.seconds.tv_nsec; + intmax_t sum_ns = orig_ns + pc.rel.ns; + int normalized_ns = (sum_ns % BILLION + BILLION) % BILLION; + int d4 = (sum_ns - normalized_ns) / BILLION; + intmax_t d1, t1, d2, t2, t3, t4; + if (INT_MULTIPLY_WRAPV (pc.rel.hour, 60 * 60, &d1) + || INT_ADD_WRAPV (Start, d1, &t1) + || INT_MULTIPLY_WRAPV (pc.rel.minutes, 60, &d2) + || INT_ADD_WRAPV (t1, d2, &t2) + || INT_ADD_WRAPV (t2, pc.rel.seconds, &t3) + || INT_ADD_WRAPV (t3, d4, &t4) + || time_overflow (t4)) + { + if (pc.parse_datetime_debug) + dbg_printf (_("error: adding relative time caused an " + "overflow\n")); + goto fail; + } - result->tv_sec = t5; + result->tv_sec = t4; result->tv_nsec = normalized_ns; + + if (pc.parse_datetime_debug + && (pc.rel.hour | pc.rel.minutes | pc.rel.seconds | pc.rel.ns)) + { + dbg_printf (_("after time adjustment (%+"PRIdMAX" hours, " + "%+"PRIdMAX" minutes, " + "%+"PRIdMAX" seconds, %+d ns),\n"), + pc.rel.hour, pc.rel.minutes, pc.rel.seconds, + pc.rel.ns); + dbg_printf (_(" new time = %"PRIdMAX" epoch-seconds\n"), t4); + + /* Warn about crossing DST due to time adjustment. + Example: https://bugs.gnu.org/8357 + env TZ=Europe/Helsinki \ + date --debug \ + -d 'Mon Mar 28 00:36:07 2011 EEST 24 hours ago' + + This case is different than DST changes due to days adjustment, + i.e., "1 day ago" vs "24 hours ago" are calculated in different + places. + + 'tm.tm_isdst' contains the date after date adjustment. */ + struct tm lmt; + if (tm.tm_isdst != -1 && localtime_rz (tz, &result->tv_sec, &lmt) + && tm.tm_isdst != lmt.tm_isdst) + dbg_printf (_("warning: daylight saving time changed after " + "time adjustment\n")); + } } } - goto done; - - fail: - ok = false; - done: - if (ok && pc.parse_datetime_debug) + if (pc.parse_datetime_debug) { - const long int otz = get_effective_timezone (tz); - const char *tz_src; - - if (tzstring) - { - /* Special case: using 'date -u' simply set TZ=UTC0 */ - if (STREQ (tzstring, "UTC0")) - { - tz_src = _("TZ=UTC0 environment value or -u"); - } - else - { - snprintf (dbg_tm, sizeof(dbg_tm), - _("TZ=\"%s\" environment value"), tzstring); - tz_src = dbg_tm; - } - } + /* Special case: using 'date -u' simply set TZ=UTC0 */ + if (! tzstring) + dbg_printf (_("timezone: system default\n")); + else if (STREQ (tzstring, "UTC0")) + dbg_printf (_("timezone: Universal Time\n")); else + dbg_printf (_("timezone: TZ=\"%s\" environment value\n"), tzstring); + + intmax_t sec = result->tv_sec; + int nsec = result->tv_nsec; + dbg_printf (_("final: %"PRIdMAX".%09d (epoch-seconds)\n"), + sec, nsec); + + struct tm gmt, lmt; + bool got_utc = !!gmtime_r (&result->tv_sec, &gmt); + if (got_utc) + dbg_printf (_("final: %s (UTC)\n"), + debug_strfdatetime (&gmt, NULL, + dbg_tm, sizeof dbg_tm)); + if (localtime_rz (tz, &result->tv_sec, &lmt)) { - tz_src = _("system default"); - } - - if (pc.parse_datetime_debug) - { - dbg_printf (_("output timezone: %+03d:%02d (set from %s)\n"), - (int)(otz/60), abs ((int)otz)%60, tz_src); - - - dbg_printf (_("final: %ld.%09ld (epoch-seconds)\n"), - result->tv_sec,result->tv_nsec); - - struct tm gmt, lmt; - if (gmtime_r (&result->tv_sec, &gmt)) - dbg_printf (_("final: %s (UTC0)\n"), - debug_strfdatetime (&gmt, NULL, - dbg_tm, sizeof dbg_tm)); - if (localtime_rz (tz, &result->tv_sec, &lmt)) - dbg_printf (_("final: %s (output timezone TZ=%+03d:%02d)\n"), +#ifdef HAVE_TM_GMTOFF + bool got_utcoff = true; + long int utcoff = lmt.tm_gmtoff; +#else + bool got_utcoff = got_utc; + int utcoff; + if (got_utcoff) + utcoff = tm_diff (&lmt, &gmt); +#endif + if (got_utcoff) + dbg_printf (_("final: %s (UTC%s)\n"), debug_strfdatetime (&lmt, NULL, dbg_tm, sizeof dbg_tm), - (int) (otz / 60), abs ((int) (otz % 60))); + time_zone_str (utcoff, time_zone_buf)); + else + dbg_printf (_("final: %s (unknown time zone offset)\n"), + debug_strfdatetime (&lmt, NULL, dbg_tm, sizeof dbg_tm)); } } + ok = true; + + fail: if (tz != tzdefault) tzfree (tz); free (tz1alloc); @@ -2414,14 +2383,16 @@ main (int ac, char **av) printf ("Bad format - couldn't convert.\n"); else if (! (tm = localtime (&d.tv_sec))) { - long int sec = d.tv_sec; - printf ("localtime (%ld) failed\n", sec); + intmax_t sec = d.tv_sec; + printf ("localtime (%"PRIdMAX") failed\n", sec); } else { int ns = d.tv_nsec; - printf ("%04ld-%02d-%02d %02d:%02d:%02d.%09d\n", - tm->tm_year + 1900L, tm->tm_mon + 1, tm->tm_mday, + char tm_year_buf[TM_YEAR_BUFSIZE]; + printf ("%s-%02d-%02d %02d:%02d:%02d.%09d\n", + tm_year_str (tm->tm_year, tm_year_buf), + tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, ns); } printf ("\t> "); diff --git a/m4/parse-datetime.m4 b/m4/parse-datetime.m4 index 33ce43a..c01d237 100644 --- a/m4/parse-datetime.m4 +++ b/m4/parse-datetime.m4 @@ -1,4 +1,4 @@ -# parse-datetime.m4 serial 21 +# parse-datetime.m4 serial 22 dnl Copyright (C) 2002-2006, 2008-2017 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, @@ -36,20 +36,4 @@ AC_DEFUN([gl_PARSE_DATETIME], AC_STRUCT_TIMEZONE AC_REQUIRE([gl_CLOCK_TIME]) AC_REQUIRE([gl_TM_GMTOFF]) - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE([[ -#include /* for time_t */ -#include /* for CHAR_BIT, LONG_MIN, LONG_MAX */ -#define TYPE_MINIMUM(t) \ - ((t) ((t) 0 < (t) -1 ? (t) 0 : ~ TYPE_MAXIMUM (t))) -#define TYPE_MAXIMUM(t) \ - ((t) ((t) 0 < (t) -1 \ - ? (t) -1 \ - : ((((t) 1 << (sizeof (t) * CHAR_BIT - 2)) - 1) * 2 + 1))) -typedef int verify_min[2 * (LONG_MIN <= TYPE_MINIMUM (time_t)) - 1]; -typedef int verify_max[2 * (TYPE_MAXIMUM (time_t) <= LONG_MAX) - 1]; - ]])], - [AC_DEFINE([TIME_T_FITS_IN_LONG_INT], [1], - [Define to 1 if all 'time_t' values fit in a 'long int'.]) - ]) ]) diff --git a/modules/parse-datetime b/modules/parse-datetime index 1b92db0..5dc6d45 100644 --- a/modules/parse-datetime +++ b/modules/parse-datetime @@ -15,6 +15,7 @@ stdbool gettime gettext-h intprops +inttypes mktime setenv strftime -- 2.7.4