Files
vlibc/src/time/tzset.c
T

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) */