Resolves: RHEL-108555,RHEL-108568,RHEL-108576,RHEL-108584,RHEL-108596,RHEL-108598,RHEL-109096,RHEL-109488,RHEL-111065,RHEL-31756,RHEL-50103
276 lines
11 KiB
Diff
276 lines
11 KiB
Diff
From e4b1932cd1c0ad2d42a6271cb651e6979b5962ed Mon Sep 17 00:00:00 2001
|
|
From: Yu Watanabe <watanabe.yu+github@gmail.com>
|
|
Date: Tue, 14 Feb 2023 03:39:15 +0900
|
|
Subject: [PATCH] time-util: make parse_timestamp() use the RFC-822/ISO 8601
|
|
standard timezone spec
|
|
|
|
If the timezone is specified with a number e.g. +0900 or so, then
|
|
let's parse the time as a UTC, and adjust it with the specified time
|
|
shift.
|
|
|
|
Otherwise, if an area has timezone change, e.g.
|
|
---
|
|
Africa/Casablanca Sun Jun 17 01:59:59 2018 UT = Sun Jun 17 01:59:59 2018 +00 isdst=0 gmtoff=0
|
|
Africa/Casablanca Sun Jun 17 02:00:00 2018 UT = Sun Jun 17 03:00:00 2018 +01 isdst=1 gmtoff=3600
|
|
Africa/Casablanca Sun Oct 28 01:59:59 2018 UT = Sun Oct 28 02:59:59 2018 +01 isdst=1 gmtoff=3600
|
|
Africa/Casablanca Sun Oct 28 02:00:00 2018 UT = Sun Oct 28 03:00:00 2018 +01 isdst=0 gmtoff=3600
|
|
---
|
|
then we could not determine isdst from the timezone (+01 in the above)
|
|
and mktime() will provide wrong results.
|
|
|
|
Fixes #26370.
|
|
|
|
(cherry picked from commit 7a9afae6040af0417d893328cb44b622dcdcb94f)
|
|
|
|
Related: RHEL-109488
|
|
---
|
|
src/basic/time-util.c | 174 +++++++++++++++++++++++++++---------------
|
|
1 file changed, 112 insertions(+), 62 deletions(-)
|
|
|
|
diff --git a/src/basic/time-util.c b/src/basic/time-util.c
|
|
index 92928e88a4..6fbbc2f655 100644
|
|
--- a/src/basic/time-util.c
|
|
+++ b/src/basic/time-util.c
|
|
@@ -609,7 +609,14 @@ char *format_timespan(char *buf, size_t l, usec_t t, usec_t accuracy) {
|
|
return buf;
|
|
}
|
|
|
|
-static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) {
|
|
+static int parse_timestamp_impl(
|
|
+ const char *t,
|
|
+ bool with_tz,
|
|
+ bool utc,
|
|
+ int isdst,
|
|
+ long gmtoff,
|
|
+ usec_t *ret) {
|
|
+
|
|
static const struct {
|
|
const char *name;
|
|
const int nr;
|
|
@@ -630,11 +637,11 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) {
|
|
{ "Sat", 6 },
|
|
};
|
|
|
|
- const char *k, *utc = NULL;
|
|
- struct tm tm, copy;
|
|
usec_t usec, plus = 0, minus = 0;
|
|
- int r, weekday = -1, isdst = -1;
|
|
+ int r, weekday = -1;
|
|
unsigned fractional = 0;
|
|
+ const char *k;
|
|
+ struct tm tm, copy;
|
|
time_t sec;
|
|
|
|
/* Allowed syntaxes:
|
|
@@ -707,46 +714,6 @@ static int parse_timestamp_impl(const char *t, usec_t *ret, bool with_tz) {
|
|
|
|
goto finish;
|
|
}
|
|
-
|
|
- /* See if the timestamp is suffixed with UTC */
|
|
- utc = endswith_no_case(t, " UTC");
|
|
- if (utc)
|
|
- t = strndupa_safe(t, utc - t);
|
|
- else {
|
|
- const char *e = NULL;
|
|
- int j;
|
|
-
|
|
- tzset();
|
|
-
|
|
- /* See if the timestamp is suffixed by either the DST or non-DST local timezone. Note
|
|
- * that we only support the local timezones here, nothing else. Not because we
|
|
- * wouldn't want to, but simply because there are no nice APIs available to cover
|
|
- * this. By accepting the local time zone strings, we make sure that all timestamps
|
|
- * written by format_timestamp() can be parsed correctly, even though we don't
|
|
- * support arbitrary timezone specifications. */
|
|
-
|
|
- for (j = 0; j <= 1; j++) {
|
|
-
|
|
- if (isempty(tzname[j]))
|
|
- continue;
|
|
-
|
|
- e = endswith_no_case(t, tzname[j]);
|
|
- if (!e)
|
|
- continue;
|
|
- if (e == t)
|
|
- continue;
|
|
- if (e[-1] != ' ')
|
|
- continue;
|
|
-
|
|
- break;
|
|
- }
|
|
-
|
|
- if (IN_SET(j, 0, 1)) {
|
|
- /* Found one of the two timezones specified. */
|
|
- t = strndupa_safe(t, e - t - 1);
|
|
- isdst = j;
|
|
- }
|
|
- }
|
|
}
|
|
|
|
sec = (time_t) (usec / USEC_PER_SEC);
|
|
@@ -864,9 +831,32 @@ parse_usec:
|
|
return -EINVAL;
|
|
|
|
from_tm:
|
|
+ assert(plus == 0);
|
|
+ assert(minus == 0);
|
|
+
|
|
if (weekday >= 0 && tm.tm_wday != weekday)
|
|
return -EINVAL;
|
|
|
|
+ if (gmtoff < 0) {
|
|
+ plus = -gmtoff * USEC_PER_SEC;
|
|
+
|
|
+ /* If gmtoff is negative, the string maye be too old to be parsed as UTC.
|
|
+ * E.g. 1969-12-31 23:00:00 -06 == 1970-01-01 05:00:00 UTC
|
|
+ * We assumed that gmtoff is in the range of -24:00…+24:00, hence the only date we need to
|
|
+ * handle here is 1969-12-31. So, let's shift the date with one day, then subtract the shift
|
|
+ * later. */
|
|
+ if (tm.tm_year == 69 && tm.tm_mon == 11 && tm.tm_mday == 31) {
|
|
+ /* Thu 1970-01-01-00:00:00 */
|
|
+ tm.tm_year = 70;
|
|
+ tm.tm_mon = 0;
|
|
+ tm.tm_mday = 1;
|
|
+ tm.tm_wday = 4;
|
|
+ tm.tm_yday = 0;
|
|
+ minus = USEC_PER_DAY;
|
|
+ }
|
|
+ } else
|
|
+ minus = gmtoff * USEC_PER_SEC;
|
|
+
|
|
sec = mktime_or_timegm(&tm, utc);
|
|
if (sec < 0)
|
|
return -EINVAL;
|
|
@@ -889,24 +879,93 @@ finish:
|
|
return 0;
|
|
}
|
|
|
|
+static int parse_timestamp_with_tz(const char *t, size_t len, bool utc, int isdst, long gmtoff, usec_t *ret) {
|
|
+ _cleanup_free_ char *buf = NULL;
|
|
+
|
|
+ assert(t);
|
|
+ assert(len > 0);
|
|
+
|
|
+ buf = strndup(t, len);
|
|
+ if (!buf)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ return parse_timestamp_impl(buf, /* with_tz = */ true, utc, isdst, gmtoff, ret);
|
|
+}
|
|
+
|
|
+static int parse_timestamp_maybe_with_tz(const char *t, size_t len, bool valid_tz, usec_t *ret) {
|
|
+ assert(t);
|
|
+ assert(len > 0);
|
|
+
|
|
+ tzset();
|
|
+
|
|
+ for (int j = 0; j <= 1; j++) {
|
|
+ if (isempty(tzname[j]))
|
|
+ continue;
|
|
+
|
|
+ if (!streq(t + len + 1, tzname[j]))
|
|
+ continue;
|
|
+
|
|
+ /* The specified timezone matches tzname[] of the local timezone. */
|
|
+ return parse_timestamp_with_tz(t, len, /* utc = */ false, /* isdst = */ j, /* gmtoff = */ 0, ret);
|
|
+ }
|
|
+
|
|
+ if (valid_tz)
|
|
+ /* We know that the specified timezone is a valid zoneinfo (e.g. Asia/Tokyo). So, simply drop
|
|
+ * the timezone and parse the remaining string as a local time. */
|
|
+ return parse_timestamp_with_tz(t, len, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret);
|
|
+
|
|
+ return parse_timestamp_impl(t, /* with_tz = */ false, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret);
|
|
+}
|
|
+
|
|
typedef struct ParseTimestampResult {
|
|
usec_t usec;
|
|
int return_value;
|
|
} ParseTimestampResult;
|
|
|
|
int parse_timestamp(const char *t, usec_t *ret) {
|
|
- char *last_space, *tz = NULL;
|
|
ParseTimestampResult *shared, tmp;
|
|
+ const char *k, *tz, *space;
|
|
+ struct tm tm;
|
|
int r;
|
|
|
|
assert(t);
|
|
|
|
- last_space = strrchr(t, ' ');
|
|
- if (last_space != NULL && timezone_is_valid(last_space + 1, LOG_DEBUG))
|
|
- tz = last_space + 1;
|
|
+ space = strrchr(t, ' ');
|
|
+ if (!space)
|
|
+ return parse_timestamp_impl(t, /* with_tz = */ false, /* utc = */ false, /* isdst = */ -1, /* gmtoff = */ 0, ret);
|
|
|
|
- if (!tz || endswith_no_case(t, " UTC"))
|
|
- return parse_timestamp_impl(t, ret, false);
|
|
+ /* The string starts with space. */
|
|
+ if (space == t)
|
|
+ return -EINVAL;
|
|
+
|
|
+ /* Shortcut, parse the string as UTC. */
|
|
+ if (streq(space + 1, "UTC"))
|
|
+ return parse_timestamp_with_tz(t, space - t, /* utc = */ true, /* isdst = */ -1, /* gmtoff = */ 0, ret);
|
|
+
|
|
+ /* If the timezone is compatible with RFC-822/ISO 8601 (e.g. +06, or -03:00) then parse the string as
|
|
+ * UTC and shift the result. */
|
|
+ k = strptime(space + 1, "%z", &tm);
|
|
+ if (k && *k == '\0') {
|
|
+ /* glibc accepts gmtoff more than 24 hours, but we refuse it. */
|
|
+ if ((usec_t) labs(tm.tm_gmtoff) > USEC_PER_DAY / USEC_PER_SEC)
|
|
+ return -EINVAL;
|
|
+
|
|
+ return parse_timestamp_with_tz(t, space - t, /* utc = */ true, /* isdst = */ -1, /* gmtoff = */ tm.tm_gmtoff, ret);
|
|
+ }
|
|
+
|
|
+ /* If the last word is not a timezone file (e.g. Asia/Tokyo), then let's check if it matches
|
|
+ * tzname[] of the local timezone, e.g. JST or CEST. */
|
|
+ if (!timezone_is_valid(space + 1, LOG_DEBUG))
|
|
+ return parse_timestamp_maybe_with_tz(t, space - t, /* valid_tz = */ false, ret);
|
|
+
|
|
+ /* Shortcut. If the current $TZ is equivalent to the specified timezone, it is not necessary to fork
|
|
+ * the process. */
|
|
+ tz = getenv("TZ");
|
|
+ if (tz && *tz == ':' && streq(tz + 1, space + 1))
|
|
+ return parse_timestamp_maybe_with_tz(t, space - t, /* valid_tz = */ true, ret);
|
|
+
|
|
+ /* Otherwise, to avoid polluting the current environment variables, let's fork the process and set
|
|
+ * the specified timezone in the child process. */
|
|
|
|
shared = mmap(NULL, sizeof *shared, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
|
|
if (shared == MAP_FAILED)
|
|
@@ -918,11 +977,10 @@ int parse_timestamp(const char *t, usec_t *ret) {
|
|
return r;
|
|
}
|
|
if (r == 0) {
|
|
- bool with_tz = true;
|
|
- char *colon_tz;
|
|
+ const char *colon_tz;
|
|
|
|
/* tzset(3) says $TZ should be prefixed with ":" if we reference timezone files */
|
|
- colon_tz = strjoina(":", tz);
|
|
+ colon_tz = strjoina(":", space + 1);
|
|
|
|
if (setenv("TZ", colon_tz, 1) != 0) {
|
|
shared->return_value = negative_errno();
|
|
@@ -931,15 +989,7 @@ int parse_timestamp(const char *t, usec_t *ret) {
|
|
|
|
tzset();
|
|
|
|
- /* If there is a timezone that matches the tzname fields, leave the parsing to the implementation.
|
|
- * Otherwise just cut it off. */
|
|
- with_tz = !STR_IN_SET(tz, tzname[0], tzname[1]);
|
|
-
|
|
- /* Cut off the timezone if we don't need it. */
|
|
- if (with_tz)
|
|
- t = strndupa_safe(t, last_space - t);
|
|
-
|
|
- shared->return_value = parse_timestamp_impl(t, &shared->usec, with_tz);
|
|
+ shared->return_value = parse_timestamp_maybe_with_tz(t, space - t, /* valid_tz = */ true, &shared->usec);
|
|
|
|
_exit(EXIT_SUCCESS);
|
|
}
|