#ifdef HAVE_CONFIG_H #include #endif #include #include #include #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 ); 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) */