653 lines
15 KiB
C
653 lines
15 KiB
C
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include <time.h>
|
|
|
|
#include "time_impl.h"
|
|
|
|
/*
|
|
* POSIX TZ engine (level-independent core of tzset/localtime).
|
|
*
|
|
* The one parsed state object mirrors the caller-visible libc timezone
|
|
* globals: tzset() re-reads the TZ environment variable on demand and
|
|
* (at level 2) republishes tzname/daylight/timezone from the result, while
|
|
* localtime/mktime consume the state lazily through __tzset_lazy. Parsing
|
|
* is deliberately lenient-fail: any syntax error, an empty TZ, or no TZ at
|
|
* all yields the UTC default (std "UTC", zero offset, no DST), never a
|
|
* crash and never a partial zone.
|
|
*
|
|
* POSIX TZ grammar (the subset vlibc accepts):
|
|
* std offset[dst[offset][,start[/time],end[/time]]]
|
|
* std/dst are 3+ alphabetic characters (or <quoted>); offset is
|
|
* [+-]hh[:mm[:ss]], the amount ADDED to local time to reach UTC, so the
|
|
* stored EAST-of-UTC std_off is its negation ("EST5" -> std_off -18000).
|
|
* An omitted dst offset defaults to standard plus one hour. Rules use the
|
|
* Mm.w.d, Jn and n spellings with an optional /time (default 02:00:00);
|
|
* when DST is present but no rules are given the US M3.2.0/M11.1.0 pair is
|
|
* assumed.
|
|
*/
|
|
|
|
static struct tz_state tz_state;
|
|
static int tz_parsed;
|
|
|
|
/* Euclidean helpers (same definitions as mktime.c, kept per-TU static). */
|
|
static long long
|
|
floor_div(long long a, long long b)
|
|
{
|
|
long long q = a / b;
|
|
long long r = a % b;
|
|
|
|
if (r != 0 && ((r < 0) != (b < 0)))
|
|
{
|
|
q -= 1;
|
|
}
|
|
return q;
|
|
}
|
|
|
|
static long long
|
|
floor_mod(long long a, long long b)
|
|
{
|
|
long long r = a % b;
|
|
|
|
if (r != 0 && ((r < 0) != (b < 0)))
|
|
{
|
|
r += b;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
static int
|
|
tz_isdigit(char c)
|
|
{
|
|
return c >= '0' && c <= '9';
|
|
}
|
|
|
|
static int
|
|
tz_isalpha(char c)
|
|
{
|
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
|
}
|
|
|
|
static int
|
|
tz_is_leap(int y)
|
|
{
|
|
return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
|
|
}
|
|
|
|
/*
|
|
* Parse a run of digits at *ip into *out; the run must be non-empty.
|
|
*/
|
|
static int
|
|
tz_parse_digits(const char *s, int *ip, long *out)
|
|
{
|
|
int i = *ip;
|
|
long v = 0;
|
|
|
|
if (!tz_isdigit(s[i]))
|
|
{
|
|
return 0;
|
|
}
|
|
while (tz_isdigit(s[i]))
|
|
{
|
|
v = v * 10 + (long)(s[i] - '0');
|
|
i++;
|
|
}
|
|
*out = v;
|
|
*ip = i;
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Parse an offset [+-]hh[:mm[:ss]] into signed seconds ("added to local
|
|
* time to reach UTC"; negation to the east-of-UTC convention happens in
|
|
* the caller).
|
|
*/
|
|
static int
|
|
tz_parse_offset(const char *s, int *ip, long *out)
|
|
{
|
|
int i = *ip;
|
|
int neg = 0;
|
|
long h;
|
|
long m = 0;
|
|
long sec = 0;
|
|
|
|
if (s[i] == '+' || s[i] == '-')
|
|
{
|
|
neg = (s[i] == '-');
|
|
i++;
|
|
}
|
|
if (!tz_parse_digits(s, &i, &h))
|
|
{
|
|
return 0;
|
|
}
|
|
if (s[i] == ':')
|
|
{
|
|
i++;
|
|
if (!tz_parse_digits(s, &i, &m))
|
|
{
|
|
return 0;
|
|
}
|
|
if (s[i] == ':')
|
|
{
|
|
i++;
|
|
if (!tz_parse_digits(s, &i, &sec))
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
*out = (h * 3600 + m * 60 + sec) * (neg ? -1 : 1);
|
|
*ip = i;
|
|
return 1;
|
|
}
|
|
|
|
/* Parse a rule transition time hh[:mm[:ss]] (no sign; seconds of day). */
|
|
static int
|
|
tz_parse_rule_time(const char *s, int *ip, long *out)
|
|
{
|
|
int i = *ip;
|
|
long h;
|
|
long m = 0;
|
|
long sec = 0;
|
|
|
|
if (!tz_parse_digits(s, &i, &h) || h > 24)
|
|
{
|
|
return 0;
|
|
}
|
|
if (s[i] == ':')
|
|
{
|
|
i++;
|
|
if (!tz_parse_digits(s, &i, &m) || m > 59)
|
|
{
|
|
return 0;
|
|
}
|
|
if (s[i] == ':')
|
|
{
|
|
i++;
|
|
if (!tz_parse_digits(s, &i, &sec) || sec > 59)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
*out = h * 3600 + m * 60 + sec;
|
|
*ip = i;
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Parse a zone name: either <...> (any bytes, stored truncated to the
|
|
* buffer) or 3+ alphabetic characters.
|
|
*/
|
|
static int
|
|
tz_parse_name(const char *s, int *ip, char *out, size_t cap)
|
|
{
|
|
int i = *ip;
|
|
size_t n = 0;
|
|
|
|
if (s[i] == '<')
|
|
{
|
|
i++;
|
|
while (s[i] != '\0' && s[i] != '>')
|
|
{
|
|
if (n + 1 < cap)
|
|
{
|
|
out[n++] = s[i];
|
|
}
|
|
i++;
|
|
}
|
|
if (s[i] != '>')
|
|
{
|
|
return 0;
|
|
}
|
|
i++;
|
|
if (n == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (tz_isalpha(s[i]))
|
|
{
|
|
if (n + 1 < cap)
|
|
{
|
|
out[n++] = s[i];
|
|
}
|
|
i++;
|
|
}
|
|
if (n < 3)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
out[n] = '\0';
|
|
*ip = i;
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Parse one transition rule (Mm.w.d, Jn, or n), optionally followed by a
|
|
* /time suffix; an omitted time defaults to 02:00:00 (7200). Ranges are
|
|
* validated so a malformed rule degrades to the UTC default.
|
|
*/
|
|
static int
|
|
tz_parse_rule(const char *s, int *ip, struct tz_rule *r)
|
|
{
|
|
int i = *ip;
|
|
long v;
|
|
|
|
r->kind = 0;
|
|
r->month = 0;
|
|
r->week = 0;
|
|
r->wday = 0;
|
|
r->day = 0;
|
|
r->time = 7200;
|
|
if (s[i] == 'M')
|
|
{
|
|
i++;
|
|
if (!tz_parse_digits(s, &i, &v) || v < 1 || v > 12)
|
|
{
|
|
return 0;
|
|
}
|
|
r->month = (int)v;
|
|
if (s[i] != '.')
|
|
{
|
|
return 0;
|
|
}
|
|
i++;
|
|
if (!tz_parse_digits(s, &i, &v) || v < 1 || v > 5)
|
|
{
|
|
return 0;
|
|
}
|
|
r->week = (int)v;
|
|
if (s[i] != '.')
|
|
{
|
|
return 0;
|
|
}
|
|
i++;
|
|
if (!tz_parse_digits(s, &i, &v) || v > 6)
|
|
{
|
|
return 0;
|
|
}
|
|
r->wday = (int)v;
|
|
}
|
|
else if (s[i] == 'J')
|
|
{
|
|
i++;
|
|
if (!tz_parse_digits(s, &i, &v) || v < 1 || v > 365)
|
|
{
|
|
return 0;
|
|
}
|
|
r->kind = 1;
|
|
r->day = v;
|
|
}
|
|
else
|
|
{
|
|
if (!tz_parse_digits(s, &i, &v) || v < 0 || v > 365)
|
|
{
|
|
return 0;
|
|
}
|
|
r->kind = 2;
|
|
r->day = v;
|
|
}
|
|
if (s[i] == '/')
|
|
{
|
|
i++;
|
|
if (!tz_parse_rule_time(s, &i, &r->time))
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
*ip = i;
|
|
return 1;
|
|
}
|
|
|
|
/* Parse the full TZ string into st; 1 on success, 0 to fall back to UTC. */
|
|
static int
|
|
tz_parse(const char *s, struct tz_state *st)
|
|
{
|
|
int i = 0;
|
|
struct tz_state tmp;
|
|
long west;
|
|
int have_dst;
|
|
|
|
memset(&tmp, 0, sizeof tmp);
|
|
tmp.start.time = 7200; /* 02:00:00 */
|
|
tmp.end.time = 7200;
|
|
if (s[0] == ':')
|
|
{
|
|
return 0; /* ":rest-of-line" spelling is not supported; UTC. */
|
|
}
|
|
if (!tz_parse_name(s, &i, tmp.std_name, sizeof tmp.std_name))
|
|
{
|
|
return 0;
|
|
}
|
|
if (!tz_parse_offset(s, &i, &west))
|
|
{
|
|
return 0;
|
|
}
|
|
tmp.std_off = -west; /* east-of-UTC storage */
|
|
|
|
have_dst = 0;
|
|
if (s[i] != '\0' && s[i] != ',')
|
|
{
|
|
if (s[i] == '<' || tz_isalpha(s[i]))
|
|
{
|
|
if (!tz_parse_name(s, &i, tmp.dst_name, sizeof tmp.dst_name))
|
|
{
|
|
return 0;
|
|
}
|
|
have_dst = 1;
|
|
if (s[i] != '\0' && s[i] != ',')
|
|
{
|
|
if (!tz_parse_offset(s, &i, &west))
|
|
{
|
|
return 0;
|
|
}
|
|
tmp.dst_off = -west;
|
|
}
|
|
else
|
|
{
|
|
tmp.dst_off = tmp.std_off + 3600;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
tmp.has_dst = have_dst;
|
|
if (have_dst)
|
|
{
|
|
if (s[i] == ',')
|
|
{
|
|
i++;
|
|
if (!tz_parse_rule(s, &i, &tmp.start) || s[i] != ',')
|
|
{
|
|
return 0;
|
|
}
|
|
i++;
|
|
if (!tz_parse_rule(s, &i, &tmp.end))
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Default transition rules: US second Sunday in March / first
|
|
* Sunday in November, both at 02:00:00 local. */
|
|
tmp.start.kind = 0;
|
|
tmp.start.month = 3;
|
|
tmp.start.week = 2;
|
|
tmp.start.wday = 0;
|
|
tmp.end.kind = 0;
|
|
tmp.end.month = 11;
|
|
tmp.end.week = 1;
|
|
tmp.end.wday = 0;
|
|
}
|
|
}
|
|
if (s[i] != '\0')
|
|
{
|
|
return 0;
|
|
}
|
|
*st = tmp;
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* tz_rule_day: day count (days since 1970-01-01) of the calendar day on
|
|
* which rule r transitions in `year`, per the rule's spelling.
|
|
*/
|
|
static long long
|
|
tz_rule_day(int year, const struct tz_rule *r)
|
|
{
|
|
if (r->kind == 0)
|
|
{
|
|
static const int mlen[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
|
|
|
if (r->week <= 4)
|
|
{
|
|
/* w-th weekday of the month. */
|
|
int first_wday = (int)floor_mod(days_from_civil(year, r->month, 1) + 4, 7);
|
|
int dom = 1 + (int)floor_mod((long long)r->wday - first_wday, 7) + 7 * (r->week - 1);
|
|
|
|
return days_from_civil(year, r->month, dom);
|
|
}
|
|
else
|
|
{
|
|
/* Last weekday of the month (week 5 = "last"). */
|
|
int dim = mlen[r->month - 1];
|
|
int last_wday;
|
|
|
|
if (r->month == 2 && tz_is_leap(year))
|
|
{
|
|
dim++;
|
|
}
|
|
last_wday = (int)floor_mod(days_from_civil(year, r->month, dim) + 4, 7);
|
|
return days_from_civil(year, r->month,
|
|
dim - (int)floor_mod((long long)last_wday - r->wday, 7));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
long long doy;
|
|
|
|
if (r->kind == 1)
|
|
{
|
|
/* Julian day, never counting 29 February. */
|
|
if (r->day <= 59)
|
|
{
|
|
doy = r->day - 1;
|
|
}
|
|
else
|
|
{
|
|
doy = r->day - (tz_is_leap(year) ? 0 : 1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
doy = r->day; /* zero-based day, leap day counted */
|
|
}
|
|
return days_from_civil(year, 1, 1) + doy;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* tz_rule_contains: is DST in effect at the given LOCAL civil time
|
|
* (seconds, in the no-offset frame all TZ math uses)?
|
|
*
|
|
* The two transitions are computed for the civil year that contains
|
|
* local_sec. When the start rule comes before the end rule in the year the
|
|
* DST interval is [start, end); when it comes after (southern hemisphere,
|
|
* DST spanning the new year) the interval wraps, so local_sec before the
|
|
* year's end transition or at/after its start transition is daylight time.
|
|
*/
|
|
// NOLINTBEGIN(bugprone-easily-swappable-parameters,bugprone-reserved-identifier)
|
|
int
|
|
tz_rule_contains(const struct tz_state *s, long long local_sec)
|
|
{
|
|
int y;
|
|
int m;
|
|
int d;
|
|
long long start_day;
|
|
long long end_day;
|
|
long long start_sec;
|
|
long long end_sec;
|
|
|
|
if (!s->has_dst)
|
|
{
|
|
return 0;
|
|
}
|
|
civil_from_days(floor_div(local_sec, 86400), &y, &m, &d);
|
|
(void)m;
|
|
(void)d;
|
|
start_day = tz_rule_day(y, &s->start);
|
|
end_day = tz_rule_day(y, &s->end);
|
|
start_sec = start_day * 86400 + s->start.time;
|
|
end_sec = end_day * 86400 + s->end.time;
|
|
if (start_day <= end_day)
|
|
{
|
|
return local_sec >= start_sec && local_sec < end_sec;
|
|
}
|
|
return local_sec >= start_sec || local_sec < end_sec;
|
|
}
|
|
|
|
/*
|
|
* tz_offset_for_local: the offset (and DST flag) for a local civil time.
|
|
* A positive isdst_hint forces daylight time, a zero hint forces standard
|
|
* time, and a negative hint ("unknown", as mktime receives from callers
|
|
* that do not know) is resolved by the zone rules.
|
|
*/
|
|
long
|
|
tz_offset_for_local(const struct tz_state *s, long long local_sec, int isdst_hint, int *isdst_out)
|
|
{
|
|
int isdst;
|
|
long off;
|
|
|
|
if (s->has_dst && isdst_hint > 0)
|
|
{
|
|
isdst = 1;
|
|
}
|
|
else if (s->has_dst && isdst_hint < 0)
|
|
{
|
|
isdst = tz_rule_contains(s, local_sec);
|
|
}
|
|
else
|
|
{
|
|
isdst = 0;
|
|
}
|
|
off = isdst ? s->dst_off : s->std_off;
|
|
if (isdst_out != NULL)
|
|
{
|
|
*isdst_out = isdst;
|
|
}
|
|
return off;
|
|
}
|
|
|
|
/*
|
|
* tz_offset_at_utc: the offset (and DST flag) in effect at the given UTC
|
|
* instant, found by the standard fixed-point iteration — assume standard
|
|
* time, convert to a local civil guess, consult the rules, and re-derive
|
|
* the offset once (DST transitions move the local time by at most one
|
|
* hour, so a second pass settles it).
|
|
*/
|
|
long
|
|
tz_offset_at_utc(const struct tz_state *s, long long utc_sec, int *isdst_out)
|
|
{
|
|
long off = s->std_off;
|
|
int isdst = 0;
|
|
|
|
if (s->has_dst)
|
|
{
|
|
isdst = tz_rule_contains(s, utc_sec + off);
|
|
off = isdst ? s->dst_off : s->std_off;
|
|
isdst = tz_rule_contains(s, utc_sec + off);
|
|
off = isdst ? s->dst_off : s->std_off;
|
|
}
|
|
if (isdst_out != NULL)
|
|
{
|
|
*isdst_out = isdst;
|
|
}
|
|
return off;
|
|
}
|
|
// NOLINTEND(bugprone-reserved-identifier)
|
|
|
|
/*
|
|
* __tzset_lazy: (re)parse TZ into the static state, but only once per
|
|
* tzset() call. Callers may run before the environment is installed, so
|
|
* getenv returning nothing is simply the UTC default.
|
|
*/
|
|
// NOLINTBEGIN(bugprone-reserved-identifier)
|
|
void
|
|
__tzset_lazy(void)
|
|
{
|
|
const char *tz;
|
|
|
|
if (tz_parsed)
|
|
{
|
|
return;
|
|
}
|
|
tz = getenv("TZ");
|
|
if (tz == NULL || tz[0] == '\0' || !tz_parse(tz, &tz_state))
|
|
{
|
|
memset(&tz_state, 0, sizeof tz_state);
|
|
strcpy(tz_state.std_name, "UTC");
|
|
}
|
|
tz_parsed = 1;
|
|
#if VLIBC_LEVEL_GE(2)
|
|
tzname[0] = tz_state.std_name;
|
|
tzname[1] = tz_state.dst_name;
|
|
daylight = tz_state.has_dst;
|
|
timezone = -tz_state.std_off; /* POSIX: seconds WEST of UTC */
|
|
#endif
|
|
}
|
|
|
|
const struct tz_state *
|
|
__tz_state(void)
|
|
{
|
|
__tzset_lazy();
|
|
return &tz_state;
|
|
}
|
|
// NOLINTEND(bugprone-reserved-identifier)
|
|
|
|
/*
|
|
* tzset: re-read TZ on the next access. The parse itself is deferred to
|
|
* __tzset_lazy so that a tzset() with no following time query is cheap.
|
|
*/
|
|
void
|
|
tzset(void)
|
|
{
|
|
tz_parsed = 0;
|
|
__tzset_lazy();
|
|
}
|
|
|
|
struct tm *
|
|
gmtime_r(const time_t *timer, struct tm *result)
|
|
{
|
|
__fill_tm((long long)*timer, 0, 0, "UTC", result);
|
|
return result;
|
|
}
|
|
|
|
struct tm *
|
|
localtime_r(const time_t *timer, struct tm *result)
|
|
{
|
|
const struct tz_state *s;
|
|
long off;
|
|
int isdst;
|
|
|
|
__tzset_lazy();
|
|
s = __tz_state();
|
|
off = tz_offset_at_utc(s, (long long)*timer, &isdst);
|
|
__fill_tm((long long)*timer + off, off, isdst, isdst ? s->dst_name : s->std_name, result);
|
|
return result;
|
|
}
|
|
|
|
static struct tm tz_static_tm;
|
|
|
|
struct tm *
|
|
gmtime(const time_t *timer)
|
|
{
|
|
return gmtime_r(timer, &tz_static_tm);
|
|
}
|
|
|
|
struct tm *
|
|
localtime(const time_t *timer)
|
|
{
|
|
return localtime_r(timer, &tz_static_tm);
|
|
}
|
|
|
|
#if VLIBC_LEVEL_GE(2)
|
|
|
|
/* POSIX timezone globals, kept in sync by tzset()/__tzset_lazy(). */
|
|
char *tzname[2];
|
|
int daylight;
|
|
long timezone;
|
|
|
|
#endif /* VLIBC_LEVEL_GE(2) */
|