1
0
mirror of https://git.savannah.gnu.org/git/gnulib.git synced 2025-08-17 12:41:05 +03:00

Add support for TZ="foo" within a date string.

Fix some bugs near time_t boundaries.  Reject dates with
out-of-range components, e.g., "Sept 31".
Include <stdlib.h>, "setenv.h", "xalloc.h".
(ISDIGIT_LOCALE): Remove; unused.
Note that the TZ and time functions used here are not reentrant.
(mktime_ok, get_tz): New functions.
(TZBUFSIZE): New constant.
(get_date): Parse leading TZ="foo".  Reject out-of-range components;.
This requires that we sometimes generate our own TZ="XXX..." setting.
This commit is contained in:
Paul Eggert
2004-10-29 20:59:53 +00:00
parent 8912e6d4dc
commit de95bdc279

View File

@@ -22,16 +22,13 @@
<rsalz@bbn.com> and Jim Berets <jberets@bbn.com> in August, 1990. <rsalz@bbn.com> and Jim Berets <jberets@bbn.com> in August, 1990.
Modified by Paul Eggert <eggert@twinsun.com> in August 1999 to do Modified by Paul Eggert <eggert@twinsun.com> in August 1999 to do
the right thing about local DST, and in February 2004 to support the right thing about local DST. Also modified by Paul Eggert
nanosecond-resolution time stamps. Unlike previous versions, this <eggert@cs.ucla.edu> in February 2004 to support
version is reentrant. */ nanosecond-resolution time stamps, and in October 2004 to support
TZ strings in dates. */
/* FIXME: Check for arithmetic overflow in all cases, not just /* FIXME: Check for arithmetic overflow in all cases, not just
some of them. some of them. */
FIXME: The current code uses 'int' to count seconds; it should use
something like 'intmax_t' to support time stamps that don't fit in
32 bits. */
#ifdef HAVE_CONFIG_H #ifdef HAVE_CONFIG_H
# include <config.h> # include <config.h>
@@ -53,6 +50,11 @@
#include <ctype.h> #include <ctype.h>
#include <limits.h> #include <limits.h>
#include <stdlib.h>
#include <string.h>
#include "setenv.h"
#include "xalloc.h"
#if STDC_HEADERS || (! defined isascii && ! HAVE_ISASCII) #if STDC_HEADERS || (! defined isascii && ! HAVE_ISASCII)
# define IN_CTYPE_DOMAIN(c) 1 # define IN_CTYPE_DOMAIN(c) 1
@@ -63,19 +65,16 @@
#define ISSPACE(c) (IN_CTYPE_DOMAIN (c) && isspace (c)) #define ISSPACE(c) (IN_CTYPE_DOMAIN (c) && isspace (c))
#define ISALPHA(c) (IN_CTYPE_DOMAIN (c) && isalpha (c)) #define ISALPHA(c) (IN_CTYPE_DOMAIN (c) && isalpha (c))
#define ISLOWER(c) (IN_CTYPE_DOMAIN (c) && islower (c)) #define ISLOWER(c) (IN_CTYPE_DOMAIN (c) && islower (c))
#define ISDIGIT_LOCALE(c) (IN_CTYPE_DOMAIN (c) && isdigit (c))
/* ISDIGIT differs from ISDIGIT_LOCALE, as follows: /* ISDIGIT differs from isdigit, as follows:
- Its arg may be any int or unsigned int; it need not be an unsigned char. - Its arg may be any int or unsigned int; it need not be an unsigned char.
- It's guaranteed to evaluate its argument exactly once. - It's guaranteed to evaluate its argument exactly once.
- It's typically faster. - It's typically faster.
POSIX says that only '0' through '9' are digits. Prefer ISDIGIT to POSIX says that only '0' through '9' are digits. Prefer ISDIGIT to
ISDIGIT_LOCALE unless it's important to use the locale's definition isdigit unless it's important to use the locale's definition
of `digit' even when the host does not conform to POSIX. */ of `digit' even when the host does not conform to POSIX. */
#define ISDIGIT(c) ((unsigned int) (c) - '0' <= 9) #define ISDIGIT(c) ((unsigned int) (c) - '0' <= 9)
#include <string.h>
#if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 8) || __STRICT_ANSI__ #if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 8) || __STRICT_ANSI__
# define __attribute__(x) # define __attribute__(x)
#endif #endif
@@ -167,7 +166,8 @@ static int yyerror (parser_control *, char *);
%} %}
/* We want a reentrant parser. */ /* We want a reentrant parser, even if the TZ manipulation and the calls to
localtime and gmtime are not reentrant. */
%pure-parser %pure-parser
%parse-param { parser_control *pc } %parse-param { parser_control *pc }
%lex-param { parser_control *pc } %lex-param { parser_control *pc }
@@ -990,6 +990,51 @@ yyerror (parser_control *pc ATTRIBUTE_UNUSED, char *s ATTRIBUTE_UNUSED)
return 0; return 0;
} }
/* If *TM0 is the old and *TM1 is the new value of a struct tm after
passing it to mktime, return true if it's OK that mktime returned T.
It's not OK if *TM0 has out-of-range members. */
static bool
mktime_ok (struct tm const *tm0, struct tm const *tm1, time_t t)
{
if (t == (time_t) -1)
{
/* Guard against falsely reporting an error when parsing a time
stamp that happens to equal (time_t) -1, on a host that
supports such a time stamp. */
tm1 = localtime (&t);
if (!tm1)
return false;
}
return ! ((tm0->tm_sec ^ tm1->tm_sec)
| (tm0->tm_min ^ tm1->tm_min)
| (tm0->tm_hour ^ tm1->tm_hour)
| (tm0->tm_mday ^ tm1->tm_mday)
| (tm0->tm_mon ^ tm1->tm_mon)
| (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 };
/* Return a copy of TZ, stored in TZBUF if it fits, and heap-allocated
otherwise. */
static char *
get_tz (char tzbuf[TZBUFSIZE])
{
char *tz = getenv ("TZ");
if (tz)
{
size_t tzsize = strlen (tz) + 1;
tz = (tzsize <= TZBUFSIZE
? memcpy (tzbuf, tz, tzsize)
: xmemdup (tz, tzsize));
}
return tz;
}
/* Parse a date/time string, storing the resulting time value into *RESULT. /* Parse a date/time string, storing the resulting time value into *RESULT.
The string itself is pointed to by P. Return true if successful. The string itself is pointed to by P. Return true if successful.
P can be an incomplete or relative time specification; if so, use P can be an incomplete or relative time specification; if so, use
@@ -1004,6 +1049,11 @@ get_date (struct timespec *result, char const *p, struct timespec const *now)
struct tm tm0; struct tm tm0;
parser_control pc; parser_control pc;
struct timespec gettime_buffer; struct timespec gettime_buffer;
unsigned char c;
bool tz_was_altered = false;
char *tz0 = NULL;
char tz0buf[TZBUFSIZE];
bool ok = true;
if (! now) if (! now)
{ {
@@ -1019,6 +1069,44 @@ get_date (struct timespec *result, char const *p, struct timespec const *now)
if (! tmp) if (! tmp)
return false; return false;
while (c = *p, ISSPACE (c))
p++;
if (strncmp (p, "TZ=\"", 4) == 0)
{
char const *tzbase = p + 4;
size_t tzsize = 1;
char const *s;
for (s = tzbase; *s; s++, tzsize++)
if (*s == '\\')
{
s++;
if (! (*s == '\\' || *s == '"'))
break;
}
else if (*s == '"')
{
char *z;
char *tz1;
char tz1buf[TZBUFSIZE];
bool large_tz = TZBUFSIZE < tzsize;
bool setenv_ok;
tz0 = get_tz (tz0buf);
z = tz1 = large_tz ? xmalloc (tzsize) : tz1buf;
for (s = tzbase; *s != '"'; s++)
*z++ = *(s += *s == '\\');
*z = '\0';
setenv_ok = setenv ("TZ", tz1, 1) == 0;
if (large_tz)
free (tz1);
if (!setenv_ok)
goto fail;
tz_was_altered = true;
p = s + 1;
}
}
pc.input = p; pc.input = p;
pc.year.value = tmp->tm_year; pc.year.value = tmp->tm_year;
pc.year.value += TM_YEAR_BASE; pc.year.value += TM_YEAR_BASE;
@@ -1106,142 +1194,173 @@ get_date (struct timespec *result, char const *p, struct timespec const *now)
} }
if (yyparse (&pc) != 0) if (yyparse (&pc) != 0)
return false; goto fail;
if (pc.timespec_seen) if (pc.timespec_seen)
{ *result = pc.seconds;
*result = pc.seconds;
return true;
}
if (1 < pc.times_seen || 1 < pc.dates_seen || 1 < pc.days_seen
|| 1 < (pc.local_zones_seen + pc.zones_seen)
|| (pc.local_zones_seen && 1 < pc.local_isdst))
return false;
tm.tm_year = to_year (pc.year) - TM_YEAR_BASE + pc.rel_year;
tm.tm_mon = pc.month - 1 + pc.rel_month;
tm.tm_mday = pc.day + pc.rel_day;
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)
return false;
tm.tm_min = pc.minutes;
tm.tm_sec = pc.seconds.tv_sec;
}
else else
{ {
tm.tm_hour = tm.tm_min = tm.tm_sec = 0; if (1 < pc.times_seen || 1 < pc.dates_seen || 1 < pc.days_seen
pc.seconds.tv_nsec = 0; || 1 < (pc.local_zones_seen + pc.zones_seen)
} || (pc.local_zones_seen && 1 < pc.local_isdst))
goto fail;
/* Let mktime deduce tm_isdst if we have an absolute time stamp, tm.tm_year = to_year (pc.year) - TM_YEAR_BASE;
or if the relative time stamp mentions days, months, or years. */ tm.tm_mon = pc.month - 1;
if (pc.dates_seen | pc.days_seen | pc.times_seen | pc.rel_day tm.tm_mday = pc.day;
| pc.rel_month | pc.rel_year) if (pc.times_seen || (pc.rels_seen && ! pc.dates_seen && ! pc.days_seen))
tm.tm_isdst = -1;
/* But if the input explicitly specifies local time with or without
DST, give mktime that information. */
if (pc.local_zones_seen)
tm.tm_isdst = pc.local_isdst;
tm0 = tm;
Start = mktime (&tm);
if (Start == (time_t) -1)
{
/* Guard against falsely reporting errors near the time_t boundaries
when parsing times in other time zones. For example, if the min
time_t value is 1970-01-01 00:00:00 UTC and we are 8 hours ahead
of UTC, then the min localtime value is 1970-01-01 08:00:00; if
we apply mktime to 1970-01-01 00:00:00 we will get an error, so
we apply mktime to 1970-01-02 08:00:00 instead and adjust the time
zone by 24 hours to compensate. This algorithm assumes that
there is no DST transition within a day of the time_t boundaries. */
if (pc.zones_seen)
{ {
tm = tm0; tm.tm_hour = to_hour (pc.hour, pc.meridian);
if (tm.tm_year <= EPOCH_YEAR - TM_YEAR_BASE) if (tm.tm_hour < 0)
{ goto fail;
tm.tm_mday++; tm.tm_min = pc.minutes;
pc.time_zone += 24 * 60; tm.tm_sec = pc.seconds.tv_sec;
} }
else else
{ {
tm.tm_mday--; tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
pc.time_zone -= 24 * 60; pc.seconds.tv_nsec = 0;
}
Start = mktime (&tm);
} }
if (Start == (time_t) -1) /* Let mktime deduce tm_isdst if we have an absolute time stamp. */
return false; if (pc.dates_seen | pc.days_seen | pc.times_seen)
} tm.tm_isdst = -1;
/* But if the input explicitly specifies local time with or without
DST, give mktime that information. */
if (pc.local_zones_seen)
tm.tm_isdst = pc.local_isdst;
tm0 = tm;
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_isdst = -1;
Start = mktime (&tm); Start = mktime (&tm);
if (Start == (time_t) -1)
return false;
}
if (pc.zones_seen) if (! mktime_ok (&tm0, &tm, Start))
{ {
long int delta = pc.time_zone * 60; if (! pc.zones_seen)
time_t t1; goto fail;
else
{
/* Guard against falsely reporting errors near the time_t
boundaries when parsing times in other time zones. For
example, suppose the input string "1969-12-31 23:00:00 -0100",
the current time zone is 8 hours ahead of UTC, and the min
time_t value is 1970-01-01 00:00:00 UTC. Then the min
localtime value is 1970-01-01 08:00:00, and mktime will
therefore fail on 1969-12-31 23:00:00. To work around the
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 tz1buf[sizeof "XXX+0:00"
+ sizeof pc.time_zone * CHAR_BIT / 3];
if (!tz_was_altered)
tz0 = get_tz (tz0buf);
sprintf (tz1buf, "XXX%s%ld:%02d", "-" + (time_zone < 0),
abs_time_zone_hour, abs_time_zone_min);
if (setenv ("TZ", tz1buf, 1) != 0)
goto fail;
tz_was_altered = true;
tm = tm0;
Start = mktime (&tm);
if (! mktime_ok (&tm0, &tm, Start))
goto fail;
}
}
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_isdst = -1;
Start = mktime (&tm);
if (Start == (time_t) -1)
goto fail;
}
if (pc.zones_seen)
{
long int delta = pc.time_zone * 60;
time_t t1;
#ifdef HAVE_TM_GMTOFF #ifdef HAVE_TM_GMTOFF
delta -= tm.tm_gmtoff; delta -= tm.tm_gmtoff;
#else #else
time_t t = Start; time_t t = Start;
struct tm const *gmt = gmtime (&t); struct tm const *gmt = gmtime (&t);
if (! gmt) if (! gmt)
return false; goto fail;
delta -= tm_diff (&tm, gmt); delta -= tm_diff (&tm, gmt);
#endif #endif
t1 = Start - delta; t1 = Start - delta;
if ((Start < t1) != (delta < 0)) if ((Start < t1) != (delta < 0))
return false; /* time_t overflow */ goto fail; /* time_t overflow */
Start = t1; Start = t1;
}
/* Add relative date. */
if (pc.rel_year | pc.rel_month | pc.rel_day)
{
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))
goto fail;
tm.tm_year = year;
tm.tm_mon = month;
tm.tm_mday = day;
Start = mktime (&tm);
if (Start == (time_t) -1)
goto fail;
}
/* Add relative hours, minutes, and seconds. On hosts that support
leap seconds, ignore the possibility of leap seconds; e.g.,
"+ 10 minutes" adds 600 seconds, even if one of them is a
leap second. Typically this is not what the user wants, but it's
too hard to do it the other way, because the time zone indicator
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 int d3 = pc.rel_seconds;
time_t t3 = t2 + d3;
long int d4 = (sum_ns - normalized_ns) / BILLION;
time_t t4 = t3 + d4;
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)))
goto fail;
result->tv_sec = t4;
result->tv_nsec = normalized_ns;
}
} }
/* Add relative hours, minutes, and seconds. Ignore leap seconds; goto done;
i.e. "+ 10 minutes" means 600 seconds, even if one of them is a
leap second. Typically this is not what the user wants, but it's
too hard to do it the other way, because the time zone indicator
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 int d3 = pc.rel_seconds;
time_t t3 = t2 + d3;
long int d4 = (sum_ns - normalized_ns) / BILLION;
time_t t4 = t3 + d4;
if ((d1 / (60 * 60) ^ pc.rel_hour) fail:
| (d2 / 60 ^ pc.rel_minutes) ok = false;
| ((t1 < t0) ^ (d1 < 0)) done:
| ((t2 < t1) ^ (d2 < 0)) if (tz_was_altered)
| ((t3 < t2) ^ (d3 < 0)) ok &= (tz0 ? setenv ("TZ", tz0, 1) : unsetenv ("TZ")) == 0;
| ((t4 < t3) ^ (d4 < 0))) if (tz0 != tz0buf)
return false; free (tz0);
return ok;
result->tv_sec = t4;
result->tv_nsec = normalized_ns;
return true;
}
} }
#if TEST #if TEST