calendarspec: fix calculation of timespec iterations that fall onto a DST change
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Tue, 29 Oct 2019 21:14:39 +0000 (22:14 +0100)
committerLennart Poettering <lennart@poettering.net>
Wed, 30 Oct 2019 16:57:01 +0000 (17:57 +0100)
If we tested a candidate time that would fall onto the DST change, and we
realized that it is now a valid time ('cause the given "hour" is missing),
we would jump to to beginning of the next bigger time period, i.e. the next
day.

mktime_or_timegm() already tells us what the next valid time is, so let's reuse
this, and continue the calculations at this point. This should allow us to
correctly jump over DST changes, but also leap seconds and similar.  It should
be OK even multiple days were removed from calendar, similarly to the
Gregorian-Julian transition. By reusing the information from normalization, we
don't have to make assumptions what the next valid time is.

Fixes #13745.

$ TZ=Australia/Sydney faketime '2019-10-06 01:50' build/systemd-analyze calendar 0/1:0/1 --iterations 20 | grep Iter
       Iter. #2: Sun 2019-10-06 01:52:00 AEST
       Iter. #3: Sun 2019-10-06 01:53:00 AEST
       Iter. #4: Sun 2019-10-06 01:54:00 AEST
       Iter. #5: Sun 2019-10-06 01:55:00 AEST
       Iter. #6: Sun 2019-10-06 01:56:00 AEST
       Iter. #7: Sun 2019-10-06 01:57:00 AEST
       Iter. #8: Sun 2019-10-06 01:58:00 AEST
       Iter. #9: Sun 2019-10-06 01:59:00 AEST
      Iter. #10: Sun 2019-10-06 03:00:00 AEDT
      Iter. #11: Sun 2019-10-06 03:01:00 AEDT
      Iter. #12: Sun 2019-10-06 03:02:00 AEDT
      Iter. #13: Sun 2019-10-06 03:03:00 AEDT
      Iter. #14: Sun 2019-10-06 03:04:00 AEDT
      Iter. #15: Sun 2019-10-06 03:05:00 AEDT
      Iter. #16: Sun 2019-10-06 03:06:00 AEDT
      Iter. #17: Sun 2019-10-06 03:07:00 AEDT
      Iter. #18: Sun 2019-10-06 03:08:00 AEDT
      Iter. #19: Sun 2019-10-06 03:09:00 AEDT
      Iter. #20: Sun 2019-10-06 03:10:00 AEDT

$ TZ=Australia/Sydney faketime 2019-10-06 build/systemd-analyze calendar 2/4:30 --iterations=3
  Original form: 2/4:30
Normalized form: *-*-* 02/4:30:00
    Next elapse: Sun 2019-10-06 06:30:00 AEDT
       (in UTC): Sat 2019-10-05 19:30:00 UTC
       From now: 5h 29min left
       Iter. #2: Sun 2019-10-06 10:30:00 AEDT
       (in UTC): Sat 2019-10-05 23:30:00 UTC
       From now: 9h left
       Iter. #3: Sun 2019-10-06 14:30:00 AEDT
       (in UTC): Sun 2019-10-06 03:30:00 UTC
       From now: 13h left

src/shared/calendarspec.c

index 1045b00..9b1582d 100644 (file)
@@ -1146,31 +1146,32 @@ static int find_matching_component(const CalendarSpec *spec, const CalendarCompo
         return r;
 }
 
-static bool tm_out_of_bounds(const struct tm *tm, bool utc) {
+static int tm_within_bounds(struct tm *tm, bool utc) {
         struct tm t;
         assert(tm);
 
-        t = *tm;
-
-        if (mktime_or_timegm(&t, utc) < 0)
-                return true;
-
         /*
          * Set an upper bound on the year so impossible dates like "*-02-31"
          * don't cause find_next() to loop forever. tm_year contains years
          * since 1900, so adjust it accordingly.
          */
         if (tm->tm_year + 1900 > MAX_YEAR)
-                return true;
+                return -ERANGE;
+
+        t = *tm;
+        if (mktime_or_timegm(&t, utc) < 0)
+                return negative_errno();
 
         /* Did any normalization take place? If so, it was out of bounds before */
-        return
-                t.tm_year != tm->tm_year ||
-                t.tm_mon != tm->tm_mon ||
-                t.tm_mday != tm->tm_mday ||
-                t.tm_hour != tm->tm_hour ||
-                t.tm_min != tm->tm_min ||
-                t.tm_sec != tm->tm_sec;
+        bool good = t.tm_year == tm->tm_year &&
+                    t.tm_mon  == tm->tm_mon  &&
+                    t.tm_mday == tm->tm_mday &&
+                    t.tm_hour == tm->tm_hour &&
+                    t.tm_min  == tm->tm_min  &&
+                    t.tm_sec  == tm->tm_sec;
+        if (!good)
+                *tm = t;
+        return good;
 }
 
 static bool matches_weekday(int weekdays_bits, const struct tm *tm, bool utc) {
@@ -1217,7 +1218,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
                 }
                 if (r < 0)
                         return r;
-                if (tm_out_of_bounds(&c, spec->utc))
+                if (tm_within_bounds(&c, spec->utc) <= 0)
                         return -ENOENT;
 
                 c.tm_mon += 1;
@@ -1228,23 +1229,27 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
                         c.tm_mday = 1;
                         c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0;
                 }
-                if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
+                if (r < 0 || (r = tm_within_bounds(&c, spec->utc)) < 0) {
                         c.tm_year++;
                         c.tm_mon = 0;
                         c.tm_mday = 1;
                         c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0;
                         continue;
                 }
+                if (r == 0)
+                        continue;
 
                 r = find_matching_component(spec, spec->day, &c, &c.tm_mday);
                 if (r > 0)
                         c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0;
-                if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
+                if (r < 0 || (r = tm_within_bounds(&c, spec->utc)) < 0) {
                         c.tm_mon++;
                         c.tm_mday = 1;
                         c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0;
                         continue;
                 }
+                if (r == 0)
+                        continue;
 
                 if (!matches_weekday(spec->weekdays_bits, &c, spec->utc)) {
                         c.tm_mday++;
@@ -1255,31 +1260,40 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
                 r = find_matching_component(spec, spec->hour, &c, &c.tm_hour);
                 if (r > 0)
                         c.tm_min = c.tm_sec = tm_usec = 0;
-                if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
+                if (r < 0 || (r = tm_within_bounds(&c, spec->utc)) < 0) {
                         c.tm_mday++;
                         c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0;
                         continue;
                 }
+                if (r == 0)
+                        /* The next hour we set might be missing if there
+                         * are time zone changes. Let's try again starting at
+                         * normalized time. */
+                        continue;
 
                 r = find_matching_component(spec, spec->minute, &c, &c.tm_min);
                 if (r > 0)
                         c.tm_sec = tm_usec = 0;
-                if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
+                if (r < 0 || (r = tm_within_bounds(&c, spec->utc)) < 0) {
                         c.tm_hour++;
                         c.tm_min = c.tm_sec = tm_usec = 0;
                         continue;
                 }
+                if (r == 0)
+                        continue;
 
                 c.tm_sec = c.tm_sec * USEC_PER_SEC + tm_usec;
                 r = find_matching_component(spec, spec->microsecond, &c, &c.tm_sec);
                 tm_usec = c.tm_sec % USEC_PER_SEC;
                 c.tm_sec /= USEC_PER_SEC;
 
-                if (r < 0 || tm_out_of_bounds(&c, spec->utc)) {
+                if (r < 0 || (r = tm_within_bounds(&c, spec->utc)) < 0) {
                         c.tm_min++;
                         c.tm_sec = tm_usec = 0;
                         continue;
                 }
+                if (r == 0)
+                        continue;
 
                 *tm = c;
                 *usec = tm_usec;