From 48d26c0164dee82f4197e886ad4df794a3a23cf7 Mon Sep 17 00:00:00 2001 From: Ivan Kurnosov Date: Wed, 6 Sep 2017 21:56:36 +1200 Subject: [PATCH] Added timezone to the CalendarSpec, parser/formatter and the timedatectl --- src/basic/calendarspec.c | 82 ++++++++++++++- src/basic/calendarspec.h | 1 + src/basic/time-util.c | 192 ++++++++++++++++++++++++----------- src/test/test-calendarspec.c | 12 +++ src/test/test-date.c | 7 ++ 5 files changed, 231 insertions(+), 63 deletions(-) diff --git a/src/basic/calendarspec.c b/src/basic/calendarspec.c index b0712a4bada..0606f0923fe 100644 --- a/src/basic/calendarspec.c +++ b/src/basic/calendarspec.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include "alloc-util.h" @@ -33,6 +34,7 @@ #include "macro.h" #include "parse-util.h" #include "string-util.h" +#include "time-util.h" #define BITS_WEEKDAYS 127 #define MIN_YEAR 1970 @@ -59,6 +61,7 @@ void calendar_spec_free(CalendarSpec *c) { free_chain(c->hour); free_chain(c->minute); free_chain(c->microsecond); + free(c->timezone); free(c); } @@ -351,7 +354,10 @@ int calendar_spec_to_string(const CalendarSpec *c, char **p) { if (c->utc) fputs_unlocked(" UTC", f); - else if (IN_SET(c->dst, 0, 1)) { + else if (c->timezone != NULL) { + fputc_unlocked(' ', f); + fputs_unlocked(c->timezone, f); + } else if (IN_SET(c->dst, 0, 1)) { /* If daylight saving is explicitly on or off, let's show the used timezone. */ @@ -888,6 +894,7 @@ int calendar_spec_from_string(const char *p, CalendarSpec **spec) { if (!c) return -ENOMEM; c->dst = -1; + c->timezone = NULL; utc = endswith_no_case(p, " UTC"); if (utc) { @@ -919,6 +926,22 @@ int calendar_spec_from_string(const char *p, CalendarSpec **spec) { if (IN_SET(j, 0, 1)) { p = strndupa(p, e - p - 1); c->dst = j; + } else { + const char *last_space; + last_space = strrchr(p, ' '); + + if (last_space != NULL) { + const char *timezone = last_space + 1; + + if (timezone_is_valid(timezone)) { + c->timezone = strdup(timezone); + if (!c->timezone) { + r = -ENOMEM; + goto fail; + } + p = strndupa(p, last_space - p); + } + } } } @@ -1293,7 +1316,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) { } } -int calendar_spec_next_usec(const CalendarSpec *spec, usec_t usec, usec_t *next) { +static int calendar_spec_next_usec_impl(const CalendarSpec *spec, usec_t usec, usec_t *next) { struct tm tm; time_t t; int r; @@ -1321,3 +1344,58 @@ int calendar_spec_next_usec(const CalendarSpec *spec, usec_t usec, usec_t *next) *next = (usec_t) t * USEC_PER_SEC + tm_usec; return 0; } + +typedef struct SpecNextResult { + usec_t next; + int return_value; +} SpecNextResult; + +int calendar_spec_next_usec(const CalendarSpec *spec, usec_t usec, usec_t *next) { + pid_t pid; + SpecNextResult *shared; + SpecNextResult tmp; + int r; + + if (isempty(spec->timezone)) + return calendar_spec_next_usec_impl(spec, usec, next); + + shared = mmap(NULL, sizeof *shared, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); + if (shared == MAP_FAILED) + return negative_errno(); + + pid = fork(); + + if (pid == -1) { + int fork_errno = errno; + (void) munmap(shared, sizeof *shared); + return -fork_errno; + } + + if (pid == 0) { + if (setenv("TZ", spec->timezone, 1) != 0) { + shared->return_value = negative_errno(); + _exit(EXIT_FAILURE); + } + + tzset(); + + shared->return_value = calendar_spec_next_usec_impl(spec, usec, &shared->next); + + _exit(EXIT_SUCCESS); + } + + r = wait_for_terminate(pid, NULL); + if (r < 0) { + (void) munmap(shared, sizeof *shared); + return r; + } + + tmp = *shared; + if (munmap(shared, sizeof *shared) != 0) + return negative_errno(); + + if (tmp.return_value == 0) + *next = tmp.next; + + return tmp.return_value; +} diff --git a/src/basic/calendarspec.h b/src/basic/calendarspec.h index 3d8798de0b0..8888251705f 100644 --- a/src/basic/calendarspec.h +++ b/src/basic/calendarspec.h @@ -40,6 +40,7 @@ typedef struct CalendarSpec { bool end_of_month; bool utc; int dst; + char *timezone; CalendarComponent *year; CalendarComponent *month; diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 68ba86f6a55..d6acdcea2c1 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -596,7 +597,7 @@ int timestamp_deserialize(const char *value, usec_t *timestamp) { return r; } -int parse_timestamp(const char *t, usec_t *usec) { +static int parse_timestamp_impl(const char *t, usec_t *usec, bool with_tz) { static const struct { const char *name; const int nr; @@ -617,7 +618,7 @@ int parse_timestamp(const char *t, usec_t *usec) { { "Sat", 6 }, }; - const char *k, *utc, *tzn = NULL; + const char *k, *utc = NULL, *tzn = NULL; struct tm tm, copy; time_t x; usec_t x_usec, plus = 0, minus = 0, ret; @@ -645,84 +646,86 @@ int parse_timestamp(const char *t, usec_t *usec) { assert(t); assert(usec); - if (t[0] == '@') + if (t[0] == '@' && !with_tz) return parse_sec(t + 1, usec); ret = now(CLOCK_REALTIME); - if (streq(t, "now")) - goto finish; + if (!with_tz) { + if (streq(t, "now")) + goto finish; - else if (t[0] == '+') { - r = parse_sec(t+1, &plus); - if (r < 0) - return r; + else if (t[0] == '+') { + r = parse_sec(t+1, &plus); + if (r < 0) + return r; - goto finish; + goto finish; - } else if (t[0] == '-') { - r = parse_sec(t+1, &minus); - if (r < 0) - return r; + } else if (t[0] == '-') { + r = parse_sec(t+1, &minus); + if (r < 0) + return r; - goto finish; + goto finish; - } else if ((k = endswith(t, " ago"))) { - t = strndupa(t, k - t); + } else if ((k = endswith(t, " ago"))) { + t = strndupa(t, k - t); - r = parse_sec(t, &minus); - if (r < 0) - return r; + r = parse_sec(t, &minus); + if (r < 0) + return r; - goto finish; + goto finish; - } else if ((k = endswith(t, " left"))) { - t = strndupa(t, k - t); + } else if ((k = endswith(t, " left"))) { + t = strndupa(t, k - t); - r = parse_sec(t, &plus); - if (r < 0) - return r; + r = parse_sec(t, &plus); + if (r < 0) + return r; - goto finish; - } + goto finish; + } - /* See if the timestamp is suffixed with UTC */ - utc = endswith_no_case(t, " UTC"); - if (utc) - t = strndupa(t, utc - t); - else { - const char *e = NULL; - int j; + /* See if the timestamp is suffixed with UTC */ + utc = endswith_no_case(t, " UTC"); + if (utc) + t = strndupa(t, utc - t); + else { + const char *e = NULL; + int j; - tzset(); + 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. */ + /* 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++) { + for (j = 0; j <= 1; j++) { - if (isempty(tzname[j])) - continue; + if (isempty(tzname[j])) + continue; - e = endswith_no_case(t, tzname[j]); - if (!e) - continue; - if (e == t) - continue; - if (e[-1] != ' ') - continue; + e = endswith_no_case(t, tzname[j]); + if (!e) + continue; + if (e == t) + continue; + if (e[-1] != ' ') + continue; - break; - } + break; + } - if (IN_SET(j, 0, 1)) { - /* Found one of the two timezones specified. */ - t = strndupa(t, e - t - 1); - dst = j; - tzn = tzname[j]; + if (IN_SET(j, 0, 1)) { + /* Found one of the two timezones specified. */ + t = strndupa(t, e - t - 1); + dst = j; + tzn = tzname[j]; + } } } @@ -732,9 +735,11 @@ int parse_timestamp(const char *t, usec_t *usec) { if (!localtime_or_gmtime_r(&x, &tm, utc)) return -EINVAL; - tm.tm_isdst = dst; - if (tzn) - tm.tm_zone = tzn; + if (!with_tz) { + tm.tm_isdst = dst; + if (tzn) + tm.tm_zone = tzn; + } if (streq(t, "today")) { tm.tm_sec = tm.tm_min = tm.tm_hour = 0; @@ -874,6 +879,71 @@ finish: return 0; } +typedef struct ParseTimestampResult { + usec_t usec; + int return_value; +} ParseTimestampResult; + +int parse_timestamp(const char *t, usec_t *usec) { + char *last_space, *timezone = NULL; + ParseTimestampResult *shared, tmp; + int r; + pid_t pid; + + last_space = strrchr(t, ' '); + + if (last_space != NULL) { + if (timezone_is_valid(last_space + 1)) { + timezone = last_space + 1; + } + } + + if (timezone == NULL || endswith_no_case(t, " UTC")) + return parse_timestamp_impl(t, usec, false); + + t = strndupa(t, last_space - t); + + shared = mmap(NULL, sizeof *shared, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); + if (shared == MAP_FAILED) + return negative_errno(); + + pid = fork(); + + if (pid == -1) { + int fork_errno = errno; + (void) munmap(shared, sizeof *shared); + return -fork_errno; + } + + if (pid == 0) { + if (setenv("TZ", timezone, 1) != 0) { + shared->return_value = negative_errno(); + _exit(EXIT_FAILURE); + } + + tzset(); + + shared->return_value = parse_timestamp_impl(t, &shared->usec, true); + + _exit(EXIT_SUCCESS); + } + + r = wait_for_terminate(pid, NULL); + if (r < 0) { + (void) munmap(shared, sizeof *shared); + return r; + } + + tmp = *shared; + if (munmap(shared, sizeof *shared) != 0) + return negative_errno(); + + if (tmp.return_value == 0) + *usec = tmp.usec; + + return tmp.return_value; +} + static char* extract_multiplier(char *p, usec_t *multiplier) { static const struct { const char *suffix; diff --git a/src/test/test-calendarspec.c b/src/test/test-calendarspec.c index a026ce4ef12..bd5bebe66d7 100644 --- a/src/test/test-calendarspec.c +++ b/src/test/test-calendarspec.c @@ -170,6 +170,8 @@ int main(int argc, char* argv[]) { test_one("annually", "*-01-01 00:00:00"); test_one("*:2/3", "*-*-* *:02/3:00"); test_one("2015-10-25 01:00:00 uTc", "2015-10-25 01:00:00 UTC"); + test_one("2015-10-25 01:00:00 Asia/Vladivostok", "2015-10-25 01:00:00 Asia/Vladivostok"); + test_one("weekly Pacific/Auckland", "Mon *-*-* 00:00:00 Pacific/Auckland"); test_one("2016-03-27 03:17:00.4200005", "2016-03-27 03:17:00.420001"); test_one("2016-03-27 03:17:00/0.42", "2016-03-27 03:17:00/0.420000"); test_one("9..11,13:00,30", "*-*-* 09..11,13:00,30:00"); @@ -219,6 +221,16 @@ int main(int argc, char* argv[]) { test_next("2017-08-06 9,11,13,15,17:00 UTC", "", 1502029800000000, 1502031600000000); test_next("2017-08-06 9..17/2:00 UTC", "", 1502029800000000, 1502031600000000); test_next("2016-12-* 3..21/6:00 UTC", "", 1482613200000001, 1482634800000000); + test_next("2017-09-24 03:30:00 Pacific/Auckland", "", 12345, 1506177000000000); + // Due to daylight saving time - 2017-09-24 02:30:00 does not exist + test_next("2017-09-24 02:30:00 Pacific/Auckland", "", 12345, -1); + test_next("2017-04-02 02:30:00 Pacific/Auckland", "", 12345, 1491053400000000); + // Confirm that even though it's a time change here (backward) 02:30 happens only once + test_next("2017-04-02 02:30:00 Pacific/Auckland", "", 1491053400000000, -1); + test_next("2017-04-02 03:30:00 Pacific/Auckland", "", 12345, 1491060600000000); + // Confirm that timezones in the Spec work regardless of current timezone + test_next("2017-09-09 20:42:00 Pacific/Auckland", "", 12345, 1504946520000000); + test_next("2017-09-09 20:42:00 Pacific/Auckland", "EET", 12345, 1504946520000000); assert_se(calendar_spec_from_string("test", &c) < 0); assert_se(calendar_spec_from_string(" utc", &c) < 0); diff --git a/src/test/test-date.c b/src/test/test-date.c index 0e7d44fade9..5cbb8bcc3ce 100644 --- a/src/test/test-date.c +++ b/src/test/test-date.c @@ -90,6 +90,10 @@ int main(int argc, char *argv[]) { test_one("yesterday"); test_one("today"); test_one("tomorrow"); + test_one_noutc("16:20 UTC"); + test_one_noutc("16:20 Asia/Seoul"); + test_one_noutc("tomorrow Asia/Seoul"); + test_one_noutc("2012-12-30 18:42 Asia/Seoul"); test_one_noutc("now"); test_one_noutc("+2d"); test_one_noutc("+2y 4d"); @@ -100,6 +104,9 @@ int main(int argc, char *argv[]) { test_should_fail("1969-12-31 UTC"); test_should_fail("-100y"); test_should_fail("today UTC UTC"); + test_should_fail("now Asia/Seoul"); + test_should_fail("+2d Asia/Seoul"); + test_should_fail("@1395716396 Asia/Seoul"); #if SIZEOF_TIME_T == 8 test_should_pass("9999-12-30 23:59:59 UTC"); test_should_fail("9999-12-31 00:00:00 UTC"); -- 2.39.5