]>
Commit | Line | Data |
---|---|---|
db9ecf05 | 1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
36697dc0 | 2 | |
9dfa81a0 | 3 | #include <ctype.h> |
11c3a366 | 4 | #include <errno.h> |
c0aebb4b | 5 | #include <limits.h> |
11c3a366 TA |
6 | #include <stddef.h> |
7 | #include <stdio.h> | |
36697dc0 | 8 | #include <stdlib.h> |
48d26c01 | 9 | #include <sys/mman.h> |
36697dc0 | 10 | |
b5efdb8a | 11 | #include "alloc-util.h" |
36697dc0 | 12 | #include "calendarspec.h" |
2b2fec7d | 13 | #include "errno-util.h" |
0d39fa9c | 14 | #include "fileio.h" |
11c3a366 | 15 | #include "macro.h" |
93cc7779 | 16 | #include "parse-util.h" |
dccca82b | 17 | #include "process-util.h" |
760877e9 | 18 | #include "sort-util.h" |
cf0fbc49 | 19 | #include "string-util.h" |
ddd6a22a | 20 | #include "strv.h" |
48d26c01 | 21 | #include "time-util.h" |
36697dc0 | 22 | |
a2eb5ea7 DC |
23 | #define BITS_WEEKDAYS 127 |
24 | #define MIN_YEAR 1970 | |
25 | #define MAX_YEAR 2199 | |
489464d0 | 26 | |
74353158 ZJS |
27 | /* An arbitrary limit on the length of the chains of components. We don't want to |
28 | * build a very long linked list, which would be slow to iterate over and might cause | |
29 | * our stack to overflow. It's unlikely that legitimate uses require more than a few | |
28423d9a | 30 | * linked components anyway. */ |
74353158 ZJS |
31 | #define CALENDARSPEC_COMPONENTS_MAX 240 |
32 | ||
a4d6d711 LP |
33 | /* Let's make sure that the microsecond component is safe to be stored in an 'int' */ |
34 | assert_cc(INT_MAX >= USEC_PER_SEC); | |
35 | ||
75db809a | 36 | static CalendarComponent* chain_free(CalendarComponent *c) { |
36697dc0 | 37 | while (c) { |
75db809a | 38 | CalendarComponent *n = c->next; |
36697dc0 LP |
39 | free(c); |
40 | c = n; | |
41 | } | |
75db809a | 42 | return NULL; |
36697dc0 LP |
43 | } |
44 | ||
fb3ba5ec YW |
45 | DEFINE_TRIVIAL_CLEANUP_FUNC(CalendarComponent*, chain_free); |
46 | ||
7c123d49 | 47 | CalendarSpec* calendar_spec_free(CalendarSpec *c) { |
0b76b4d8 LP |
48 | |
49 | if (!c) | |
7c123d49 | 50 | return NULL; |
36697dc0 | 51 | |
9eef82e5 YW |
52 | chain_free(c->year); |
53 | chain_free(c->month); | |
54 | chain_free(c->day); | |
55 | chain_free(c->hour); | |
56 | chain_free(c->minute); | |
57 | chain_free(c->microsecond); | |
48d26c01 | 58 | free(c->timezone); |
36697dc0 | 59 | |
7c123d49 | 60 | return mfree(c); |
36697dc0 LP |
61 | } |
62 | ||
93bab288 YW |
63 | static int component_compare(CalendarComponent * const *a, CalendarComponent * const *b) { |
64 | int r; | |
36697dc0 | 65 | |
93bab288 YW |
66 | r = CMP((*a)->start, (*b)->start); |
67 | if (r != 0) | |
68 | return r; | |
36697dc0 | 69 | |
93bab288 YW |
70 | r = CMP((*a)->stop, (*b)->stop); |
71 | if (r != 0) | |
72 | return r; | |
a2eb5ea7 | 73 | |
93bab288 | 74 | return CMP((*a)->repeat, (*b)->repeat); |
36697dc0 LP |
75 | } |
76 | ||
a2eb5ea7 | 77 | static void normalize_chain(CalendarComponent **c) { |
36697dc0 | 78 | CalendarComponent **b, *i, **j, *next; |
da6053d0 | 79 | size_t n = 0, k; |
36697dc0 LP |
80 | |
81 | assert(c); | |
82 | ||
a2eb5ea7 | 83 | for (i = *c; i; i = i->next) { |
36697dc0 LP |
84 | n++; |
85 | ||
a2eb5ea7 | 86 | /* |
e78fce48 | 87 | * While we're counting the chain, also normalize `stop` |
a2eb5ea7 DC |
88 | * so the length of the range is a multiple of `repeat` |
89 | */ | |
c0aebb4b | 90 | if (i->stop > i->start && i->repeat > 0) |
e78fce48 | 91 | i->stop -= (i->stop - i->start) % i->repeat; |
a2eb5ea7 | 92 | |
c9c9f6f4 LP |
93 | /* If a repeat value is specified, but it cannot even be triggered once, let's suppress |
94 | * it. | |
95 | * | |
96 | * Similar, if the stop value is the same as the start value, then let's just make this a | |
97 | * non-repeating chain element */ | |
98 | if ((i->stop > i->start && i->repeat > 0 && i->start + i->repeat > i->stop) || | |
99 | i->start == i->stop) { | |
100 | i->repeat = 0; | |
101 | i->stop = -1; | |
102 | } | |
a2eb5ea7 DC |
103 | } |
104 | ||
36697dc0 LP |
105 | if (n <= 1) |
106 | return; | |
107 | ||
cf409d15 | 108 | j = b = newa(CalendarComponent*, n); |
36697dc0 LP |
109 | for (i = *c; i; i = i->next) |
110 | *(j++) = i; | |
111 | ||
93bab288 | 112 | typesafe_qsort(b, n, component_compare); |
36697dc0 LP |
113 | |
114 | b[n-1]->next = NULL; | |
115 | next = b[n-1]; | |
116 | ||
117 | /* Drop non-unique entries */ | |
118 | for (k = n-1; k > 0; k--) { | |
963e3d83 | 119 | if (component_compare(&b[k-1], &next) == 0) { |
36697dc0 LP |
120 | free(b[k-1]); |
121 | continue; | |
122 | } | |
123 | ||
124 | b[k-1]->next = next; | |
125 | next = b[k-1]; | |
126 | } | |
127 | ||
128 | *c = next; | |
129 | } | |
130 | ||
131 | static void fix_year(CalendarComponent *c) { | |
132 | /* Turns 12 → 2012, 89 → 1989 */ | |
133 | ||
9ed794a3 | 134 | while (c) { |
e78fce48 DC |
135 | if (c->start >= 0 && c->start < 70) |
136 | c->start += 2000; | |
36697dc0 | 137 | |
e78fce48 DC |
138 | if (c->stop >= 0 && c->stop < 70) |
139 | c->stop += 2000; | |
a2eb5ea7 | 140 | |
e78fce48 DC |
141 | if (c->start >= 70 && c->start < 100) |
142 | c->start += 1900; | |
36697dc0 | 143 | |
e78fce48 DC |
144 | if (c->stop >= 70 && c->stop < 100) |
145 | c->stop += 1900; | |
a2eb5ea7 | 146 | |
482f3b54 | 147 | c = c->next; |
36697dc0 LP |
148 | } |
149 | } | |
150 | ||
151 | int calendar_spec_normalize(CalendarSpec *c) { | |
152 | assert(c); | |
153 | ||
10e859a2 LP |
154 | if (streq_ptr(c->timezone, "UTC")) { |
155 | c->utc = true; | |
156 | c->timezone = mfree(c->timezone); | |
157 | } | |
158 | ||
489464d0 | 159 | if (c->weekdays_bits <= 0 || c->weekdays_bits >= BITS_WEEKDAYS) |
36697dc0 LP |
160 | c->weekdays_bits = -1; |
161 | ||
8ea80351 DC |
162 | if (c->end_of_month && !c->day) |
163 | c->end_of_month = false; | |
164 | ||
36697dc0 LP |
165 | fix_year(c->year); |
166 | ||
a2eb5ea7 DC |
167 | normalize_chain(&c->year); |
168 | normalize_chain(&c->month); | |
169 | normalize_chain(&c->day); | |
170 | normalize_chain(&c->hour); | |
171 | normalize_chain(&c->minute); | |
172 | normalize_chain(&c->microsecond); | |
36697dc0 LP |
173 | |
174 | return 0; | |
175 | } | |
176 | ||
3c6f0300 | 177 | static bool chain_valid(CalendarComponent *c, int from, int to, bool end_of_month) { |
e127f26b ZJS |
178 | assert(to >= from); |
179 | ||
36697dc0 LP |
180 | if (!c) |
181 | return true; | |
182 | ||
a2eb5ea7 DC |
183 | /* Forbid dates more than 28 days from the end of the month */ |
184 | if (end_of_month) | |
185 | to -= 3; | |
186 | ||
e78fce48 | 187 | if (c->start < from || c->start > to) |
36697dc0 LP |
188 | return false; |
189 | ||
e127f26b ZJS |
190 | /* Avoid overly large values that could cause overflow */ |
191 | if (c->repeat > to - from) | |
192 | return false; | |
193 | ||
8ea80351 DC |
194 | /* |
195 | * c->repeat must be short enough so at least one repetition may | |
196 | * occur before the end of the interval. For dates scheduled | |
e78fce48 | 197 | * relative to the end of the month, c->start and c->stop |
a2eb5ea7 | 198 | * correspond to the Nth last day of the month. |
8ea80351 | 199 | */ |
e78fce48 DC |
200 | if (c->stop >= 0) { |
201 | if (c->stop < from || c ->stop > to) | |
a2eb5ea7 | 202 | return false; |
8ea80351 | 203 | |
e78fce48 | 204 | if (c->start + c->repeat > c->stop) |
a2eb5ea7 DC |
205 | return false; |
206 | } else { | |
e78fce48 | 207 | if (end_of_month && c->start - c->repeat < from) |
a2eb5ea7 DC |
208 | return false; |
209 | ||
e78fce48 | 210 | if (!end_of_month && c->start + c->repeat > to) |
a2eb5ea7 DC |
211 | return false; |
212 | } | |
36697dc0 LP |
213 | |
214 | if (c->next) | |
c58b1b3a | 215 | return chain_valid(c->next, from, to, end_of_month); |
36697dc0 LP |
216 | |
217 | return true; | |
218 | } | |
219 | ||
44a6b1b6 | 220 | _pure_ bool calendar_spec_valid(CalendarSpec *c) { |
36697dc0 LP |
221 | assert(c); |
222 | ||
489464d0 | 223 | if (c->weekdays_bits > BITS_WEEKDAYS) |
36697dc0 LP |
224 | return false; |
225 | ||
8ea80351 | 226 | if (!chain_valid(c->year, MIN_YEAR, MAX_YEAR, false)) |
36697dc0 LP |
227 | return false; |
228 | ||
8ea80351 | 229 | if (!chain_valid(c->month, 1, 12, false)) |
36697dc0 LP |
230 | return false; |
231 | ||
8ea80351 | 232 | if (!chain_valid(c->day, 1, 31, c->end_of_month)) |
36697dc0 LP |
233 | return false; |
234 | ||
8ea80351 | 235 | if (!chain_valid(c->hour, 0, 23, false)) |
36697dc0 LP |
236 | return false; |
237 | ||
8ea80351 | 238 | if (!chain_valid(c->minute, 0, 59, false)) |
36697dc0 LP |
239 | return false; |
240 | ||
8ea80351 | 241 | if (!chain_valid(c->microsecond, 0, 60*USEC_PER_SEC-1, false)) |
36697dc0 LP |
242 | return false; |
243 | ||
244 | return true; | |
245 | } | |
246 | ||
247 | static void format_weekdays(FILE *f, const CalendarSpec *c) { | |
248 | static const char *const days[] = { | |
249 | "Mon", | |
250 | "Tue", | |
251 | "Wed", | |
252 | "Thu", | |
253 | "Fri", | |
254 | "Sat", | |
255 | "Sun" | |
256 | }; | |
257 | ||
258 | int l, x; | |
e638d050 | 259 | bool need_comma = false; |
36697dc0 LP |
260 | |
261 | assert(f); | |
262 | assert(c); | |
489464d0 | 263 | assert(c->weekdays_bits > 0 && c->weekdays_bits <= BITS_WEEKDAYS); |
36697dc0 LP |
264 | |
265 | for (x = 0, l = -1; x < (int) ELEMENTSOF(days); x++) { | |
266 | ||
267 | if (c->weekdays_bits & (1 << x)) { | |
268 | ||
269 | if (l < 0) { | |
e638d050 | 270 | if (need_comma) |
0d536673 | 271 | fputc(',', f); |
36697dc0 | 272 | else |
e638d050 | 273 | need_comma = true; |
36697dc0 | 274 | |
0d536673 | 275 | fputs(days[x], f); |
36697dc0 LP |
276 | l = x; |
277 | } | |
278 | ||
279 | } else if (l >= 0) { | |
280 | ||
281 | if (x > l + 1) { | |
0d536673 LP |
282 | fputs(x > l + 2 ? ".." : ",", f); |
283 | fputs(days[x-1], f); | |
36697dc0 LP |
284 | } |
285 | ||
286 | l = -1; | |
287 | } | |
288 | } | |
289 | ||
290 | if (l >= 0 && x > l + 1) { | |
0d536673 LP |
291 | fputs(x > l + 2 ? ".." : ",", f); |
292 | fputs(days[x-1], f); | |
36697dc0 LP |
293 | } |
294 | } | |
295 | ||
436dd70f | 296 | static void format_chain(FILE *f, int space, const CalendarComponent *c, bool usec) { |
7c250321 | 297 | int d = usec ? (int) USEC_PER_SEC : 1; |
9904dc00 | 298 | |
36697dc0 LP |
299 | assert(f); |
300 | ||
301 | if (!c) { | |
0d536673 | 302 | fputc('*', f); |
36697dc0 LP |
303 | return; |
304 | } | |
305 | ||
482f3b54 | 306 | if (usec && c->start == 0 && c->repeat == USEC_PER_SEC && !c->next) { |
0d536673 | 307 | fputc('*', f); |
482f3b54 DC |
308 | return; |
309 | } | |
310 | ||
e78fce48 | 311 | assert(c->start >= 0); |
7c250321 | 312 | |
e78fce48 DC |
313 | fprintf(f, "%0*i", space, c->start / d); |
314 | if (c->start % d > 0) | |
315 | fprintf(f, ".%06i", c->start % d); | |
7c250321 | 316 | |
e78fce48 DC |
317 | if (c->stop > 0) |
318 | fprintf(f, "..%0*i", space, c->stop / d); | |
319 | if (c->stop % d > 0) | |
320 | fprintf(f, ".%06i", c->stop % d); | |
a2eb5ea7 | 321 | |
e78fce48 | 322 | if (c->repeat > 0 && !(c->stop > 0 && c->repeat == d)) |
7c250321 | 323 | fprintf(f, "/%i", c->repeat / d); |
a2eb5ea7 | 324 | if (c->repeat % d > 0) |
7c250321 | 325 | fprintf(f, ".%06i", c->repeat % d); |
36697dc0 | 326 | |
a2eb5ea7 | 327 | if (c->next) { |
0d536673 | 328 | fputc(',', f); |
436dd70f | 329 | format_chain(f, space, c->next, usec); |
36697dc0 LP |
330 | } |
331 | } | |
332 | ||
333 | int calendar_spec_to_string(const CalendarSpec *c, char **p) { | |
334 | char *buf = NULL; | |
335 | size_t sz = 0; | |
336 | FILE *f; | |
dacd6cee | 337 | int r; |
36697dc0 LP |
338 | |
339 | assert(c); | |
340 | assert(p); | |
341 | ||
2fe21124 | 342 | f = open_memstream_unlocked(&buf, &sz); |
36697dc0 LP |
343 | if (!f) |
344 | return -ENOMEM; | |
345 | ||
489464d0 | 346 | if (c->weekdays_bits > 0 && c->weekdays_bits <= BITS_WEEKDAYS) { |
36697dc0 | 347 | format_weekdays(f, c); |
0d536673 | 348 | fputc(' ', f); |
36697dc0 LP |
349 | } |
350 | ||
436dd70f | 351 | format_chain(f, 4, c->year, false); |
0d536673 | 352 | fputc('-', f); |
436dd70f | 353 | format_chain(f, 2, c->month, false); |
0d536673 | 354 | fputc(c->end_of_month ? '~' : '-', f); |
436dd70f | 355 | format_chain(f, 2, c->day, false); |
0d536673 | 356 | fputc(' ', f); |
436dd70f | 357 | format_chain(f, 2, c->hour, false); |
0d536673 | 358 | fputc(':', f); |
436dd70f | 359 | format_chain(f, 2, c->minute, false); |
0d536673 | 360 | fputc(':', f); |
482f3b54 | 361 | format_chain(f, 2, c->microsecond, true); |
36697dc0 | 362 | |
51ffa239 | 363 | if (c->utc) |
0d536673 | 364 | fputs(" UTC", f); |
1d6cc5d0 | 365 | else if (c->timezone) { |
0d536673 LP |
366 | fputc(' ', f); |
367 | fputs(c->timezone, f); | |
48d26c01 | 368 | } else if (IN_SET(c->dst, 0, 1)) { |
21b3a0fc LP |
369 | |
370 | /* If daylight saving is explicitly on or off, let's show the used timezone. */ | |
371 | ||
372 | tzset(); | |
373 | ||
374 | if (!isempty(tzname[c->dst])) { | |
0d536673 LP |
375 | fputc(' ', f); |
376 | fputs(tzname[c->dst], f); | |
21b3a0fc LP |
377 | } |
378 | } | |
51ffa239 | 379 | |
dacd6cee | 380 | r = fflush_and_check(f); |
05851cb9 LP |
381 | fclose(f); |
382 | ||
dacd6cee | 383 | if (r < 0) { |
36697dc0 | 384 | free(buf); |
dacd6cee | 385 | return r; |
36697dc0 LP |
386 | } |
387 | ||
36697dc0 LP |
388 | *p = buf; |
389 | return 0; | |
390 | } | |
391 | ||
392 | static int parse_weekdays(const char **p, CalendarSpec *c) { | |
393 | static const struct { | |
394 | const char *name; | |
395 | const int nr; | |
396 | } day_nr[] = { | |
397 | { "Monday", 0 }, | |
398 | { "Mon", 0 }, | |
399 | { "Tuesday", 1 }, | |
400 | { "Tue", 1 }, | |
401 | { "Wednesday", 2 }, | |
402 | { "Wed", 2 }, | |
403 | { "Thursday", 3 }, | |
404 | { "Thu", 3 }, | |
405 | { "Friday", 4 }, | |
406 | { "Fri", 4 }, | |
407 | { "Saturday", 5 }, | |
408 | { "Sat", 5 }, | |
409 | { "Sunday", 6 }, | |
410 | { "Sun", 6 } | |
411 | }; | |
412 | ||
413 | int l = -1; | |
414 | bool first = true; | |
415 | ||
416 | assert(p); | |
417 | assert(*p); | |
418 | assert(c); | |
419 | ||
420 | for (;;) { | |
da6053d0 | 421 | size_t i; |
36697dc0 | 422 | |
36697dc0 LP |
423 | for (i = 0; i < ELEMENTSOF(day_nr); i++) { |
424 | size_t skip; | |
425 | ||
426 | if (!startswith_no_case(*p, day_nr[i].name)) | |
427 | continue; | |
428 | ||
429 | skip = strlen(day_nr[i].name); | |
430 | ||
4c701096 | 431 | if (!IN_SET((*p)[skip], 0, '-', '.', ',', ' ')) |
36697dc0 LP |
432 | return -EINVAL; |
433 | ||
434 | c->weekdays_bits |= 1 << day_nr[i].nr; | |
435 | ||
436 | if (l >= 0) { | |
437 | int j; | |
438 | ||
439 | if (l > day_nr[i].nr) | |
440 | return -EINVAL; | |
441 | ||
442 | for (j = l + 1; j < day_nr[i].nr; j++) | |
443 | c->weekdays_bits |= 1 << j; | |
444 | } | |
445 | ||
446 | *p += skip; | |
447 | break; | |
448 | } | |
449 | ||
450 | /* Couldn't find this prefix, so let's assume the | |
451 | weekday was not specified and let's continue with | |
452 | the date */ | |
453 | if (i >= ELEMENTSOF(day_nr)) | |
454 | return first ? 0 : -EINVAL; | |
455 | ||
456 | /* We reached the end of the string */ | |
457 | if (**p == 0) | |
458 | return 0; | |
459 | ||
460 | /* We reached the end of the weekday spec part */ | |
461 | if (**p == ' ') { | |
462 | *p += strspn(*p, " "); | |
463 | return 0; | |
464 | } | |
465 | ||
e638d050 DC |
466 | if (**p == '.') { |
467 | if (l >= 0) | |
468 | return -EINVAL; | |
469 | ||
470 | if ((*p)[1] != '.') | |
471 | return -EINVAL; | |
472 | ||
473 | l = day_nr[i].nr; | |
6bae2fd4 | 474 | *p += 2; |
e638d050 DC |
475 | |
476 | /* Support ranges with "-" for backwards compatibility */ | |
477 | } else if (**p == '-') { | |
36697dc0 LP |
478 | if (l >= 0) |
479 | return -EINVAL; | |
480 | ||
481 | l = day_nr[i].nr; | |
6bae2fd4 DC |
482 | *p += 1; |
483 | ||
484 | } else if (**p == ',') { | |
36697dc0 | 485 | l = -1; |
6bae2fd4 DC |
486 | *p += 1; |
487 | } | |
488 | ||
482f3b54 | 489 | /* Allow a trailing comma but not an open range */ |
4c701096 | 490 | if (IN_SET(**p, 0, ' ')) { |
6bae2fd4 DC |
491 | *p += strspn(*p, " "); |
492 | return l < 0 ? 0 : -EINVAL; | |
493 | } | |
36697dc0 | 494 | |
36697dc0 LP |
495 | first = false; |
496 | } | |
497 | } | |
498 | ||
d80e5b74 ZJS |
499 | static int parse_one_number(const char *p, const char **e, unsigned long *ret) { |
500 | char *ee = NULL; | |
501 | unsigned long value; | |
502 | ||
503 | errno = 0; | |
504 | value = strtoul(p, &ee, 10); | |
505 | if (errno > 0) | |
506 | return -errno; | |
507 | if (ee == p) | |
508 | return -EINVAL; | |
509 | ||
510 | *ret = value; | |
511 | *e = ee; | |
512 | return 0; | |
513 | } | |
514 | ||
c0aebb4b | 515 | static int parse_component_decimal(const char **p, bool usec, int *res) { |
436dd70f HV |
516 | unsigned long value; |
517 | const char *e = NULL; | |
436dd70f | 518 | int r; |
36697dc0 | 519 | |
9dfa81a0 DC |
520 | if (!isdigit(**p)) |
521 | return -EINVAL; | |
522 | ||
d80e5b74 ZJS |
523 | r = parse_one_number(*p, &e, &value); |
524 | if (r < 0) | |
525 | return r; | |
36697dc0 | 526 | |
436dd70f HV |
527 | if (usec) { |
528 | if (value * USEC_PER_SEC / USEC_PER_SEC != value) | |
36697dc0 LP |
529 | return -ERANGE; |
530 | ||
436dd70f | 531 | value *= USEC_PER_SEC; |
436dd70f | 532 | |
60bf5836 DC |
533 | /* One "." is a decimal point, but ".." is a range separator */ |
534 | if (e[0] == '.' && e[1] != '.') { | |
535 | unsigned add; | |
36ff0c97 | 536 | |
436dd70f HV |
537 | e++; |
538 | r = parse_fractional_part_u(&e, 6, &add); | |
539 | if (r < 0) | |
540 | return r; | |
541 | ||
542 | if (add + value < value) | |
543 | return -ERANGE; | |
544 | value += add; | |
545 | } | |
546 | } | |
547 | ||
c0aebb4b DC |
548 | if (value > INT_MAX) |
549 | return -ERANGE; | |
550 | ||
436dd70f HV |
551 | *p = e; |
552 | *res = value; | |
553 | ||
554 | return 0; | |
555 | } | |
556 | ||
32b52369 DC |
557 | static int const_chain(int value, CalendarComponent **c) { |
558 | CalendarComponent *cc = NULL; | |
559 | ||
560 | assert(c); | |
561 | ||
4122b14b | 562 | cc = new(CalendarComponent, 1); |
32b52369 DC |
563 | if (!cc) |
564 | return -ENOMEM; | |
565 | ||
4122b14b YW |
566 | *cc = (CalendarComponent) { |
567 | .start = value, | |
568 | .stop = -1, | |
569 | .repeat = 0, | |
570 | .next = *c, | |
571 | }; | |
32b52369 DC |
572 | |
573 | *c = cc; | |
574 | ||
575 | return 0; | |
576 | } | |
577 | ||
d80e5b74 | 578 | static int calendarspec_from_time_t(CalendarSpec *c, time_t time) { |
fb3ba5ec YW |
579 | _cleanup_(chain_freep) CalendarComponent |
580 | *year = NULL, *month = NULL, *day = NULL, | |
581 | *hour = NULL, *minute = NULL, *us = NULL; | |
d80e5b74 | 582 | struct tm tm; |
d80e5b74 ZJS |
583 | int r; |
584 | ||
55a30fd4 ZJS |
585 | if (!gmtime_r(&time, &tm)) |
586 | return -ERANGE; | |
d80e5b74 | 587 | |
daa4aca1 YW |
588 | if (tm.tm_year > INT_MAX - 1900) |
589 | return -ERANGE; | |
590 | ||
d80e5b74 ZJS |
591 | r = const_chain(tm.tm_year + 1900, &year); |
592 | if (r < 0) | |
593 | return r; | |
594 | ||
595 | r = const_chain(tm.tm_mon + 1, &month); | |
596 | if (r < 0) | |
597 | return r; | |
598 | ||
599 | r = const_chain(tm.tm_mday, &day); | |
600 | if (r < 0) | |
601 | return r; | |
602 | ||
603 | r = const_chain(tm.tm_hour, &hour); | |
604 | if (r < 0) | |
605 | return r; | |
606 | ||
607 | r = const_chain(tm.tm_min, &minute); | |
608 | if (r < 0) | |
609 | return r; | |
610 | ||
611 | r = const_chain(tm.tm_sec * USEC_PER_SEC, &us); | |
612 | if (r < 0) | |
613 | return r; | |
614 | ||
615 | c->utc = true; | |
fb3ba5ec YW |
616 | c->year = TAKE_PTR(year); |
617 | c->month = TAKE_PTR(month); | |
618 | c->day = TAKE_PTR(day); | |
619 | c->hour = TAKE_PTR(hour); | |
620 | c->minute = TAKE_PTR(minute); | |
621 | c->microsecond = TAKE_PTR(us); | |
d80e5b74 ZJS |
622 | return 0; |
623 | } | |
624 | ||
74353158 | 625 | static int prepend_component(const char **p, bool usec, unsigned nesting, CalendarComponent **c) { |
c0aebb4b | 626 | int r, start, stop = -1, repeat = 0; |
436dd70f | 627 | CalendarComponent *cc; |
74353158 | 628 | const char *e = *p; |
436dd70f HV |
629 | |
630 | assert(p); | |
631 | assert(c); | |
632 | ||
74353158 ZJS |
633 | if (nesting > CALENDARSPEC_COMPONENTS_MAX) |
634 | return -ENOBUFS; | |
436dd70f | 635 | |
e78fce48 | 636 | r = parse_component_decimal(&e, usec, &start); |
436dd70f HV |
637 | if (r < 0) |
638 | return r; | |
639 | ||
a2eb5ea7 DC |
640 | if (e[0] == '.' && e[1] == '.') { |
641 | e += 2; | |
e78fce48 | 642 | r = parse_component_decimal(&e, usec, &stop); |
a2eb5ea7 DC |
643 | if (r < 0) |
644 | return r; | |
645 | ||
646 | repeat = usec ? USEC_PER_SEC : 1; | |
647 | } | |
648 | ||
436dd70f HV |
649 | if (*e == '/') { |
650 | e++; | |
651 | r = parse_component_decimal(&e, usec, &repeat); | |
652 | if (r < 0) | |
653 | return r; | |
654 | ||
655 | if (repeat == 0) | |
656 | return -ERANGE; | |
c9c9f6f4 LP |
657 | } else { |
658 | /* If no repeat value is specified for the µs component, then let's explicitly refuse ranges | |
659 | * below 1s because our default repeat granularity is beyond that. */ | |
660 | ||
c07f18ff FS |
661 | /* Overflow check */ |
662 | if (start > INT_MAX - repeat) | |
663 | return -ERANGE; | |
664 | ||
c9c9f6f4 LP |
665 | if (usec && stop >= 0 && start + repeat > stop) |
666 | return -EINVAL; | |
36697dc0 LP |
667 | } |
668 | ||
4c701096 | 669 | if (!IN_SET(*e, 0, ' ', ',', '-', '~', ':')) |
36697dc0 LP |
670 | return -EINVAL; |
671 | ||
4122b14b | 672 | cc = new(CalendarComponent, 1); |
36697dc0 LP |
673 | if (!cc) |
674 | return -ENOMEM; | |
675 | ||
4122b14b YW |
676 | *cc = (CalendarComponent) { |
677 | .start = start, | |
678 | .stop = stop, | |
679 | .repeat = repeat, | |
680 | .next = *c, | |
681 | }; | |
36697dc0 LP |
682 | |
683 | *p = e; | |
684 | *c = cc; | |
685 | ||
686 | if (*e ==',') { | |
687 | *p += 1; | |
74353158 | 688 | return prepend_component(p, usec, nesting + 1, c); |
36697dc0 LP |
689 | } |
690 | ||
691 | return 0; | |
692 | } | |
693 | ||
436dd70f | 694 | static int parse_chain(const char **p, bool usec, CalendarComponent **c) { |
fb3ba5ec | 695 | _cleanup_(chain_freep) CalendarComponent *cc = NULL; |
36697dc0 | 696 | const char *t; |
36697dc0 LP |
697 | int r; |
698 | ||
699 | assert(p); | |
700 | assert(c); | |
701 | ||
702 | t = *p; | |
703 | ||
704 | if (t[0] == '*') { | |
436dd70f HV |
705 | if (usec) { |
706 | r = const_chain(0, c); | |
707 | if (r < 0) | |
708 | return r; | |
709 | (*c)->repeat = USEC_PER_SEC; | |
710 | } else | |
711 | *c = NULL; | |
712 | ||
36697dc0 | 713 | *p = t + 1; |
36697dc0 LP |
714 | return 0; |
715 | } | |
716 | ||
74353158 | 717 | r = prepend_component(&t, usec, 0, &cc); |
fb3ba5ec | 718 | if (r < 0) |
36697dc0 | 719 | return r; |
36697dc0 LP |
720 | |
721 | *p = t; | |
fb3ba5ec | 722 | *c = TAKE_PTR(cc); |
36697dc0 LP |
723 | return 0; |
724 | } | |
725 | ||
36697dc0 | 726 | static int parse_date(const char **p, CalendarSpec *c) { |
fb3ba5ec | 727 | _cleanup_(chain_freep) CalendarComponent *first = NULL, *second = NULL, *third = NULL; |
36697dc0 LP |
728 | const char *t; |
729 | int r; | |
36697dc0 LP |
730 | |
731 | assert(p); | |
732 | assert(*p); | |
733 | assert(c); | |
734 | ||
735 | t = *p; | |
736 | ||
737 | if (*t == 0) | |
738 | return 0; | |
739 | ||
d80e5b74 ZJS |
740 | /* @TIMESTAMP — UNIX time in seconds since the epoch */ |
741 | if (*t == '@') { | |
742 | unsigned long value; | |
743 | time_t time; | |
744 | ||
745 | r = parse_one_number(t + 1, &t, &value); | |
746 | if (r < 0) | |
747 | return r; | |
748 | ||
749 | time = value; | |
750 | if ((unsigned long) time != value) | |
751 | return -ERANGE; | |
752 | ||
753 | r = calendarspec_from_time_t(c, time); | |
754 | if (r < 0) | |
755 | return r; | |
756 | ||
757 | *p = t; | |
758 | return 1; /* finito, don't parse H:M:S after that */ | |
759 | } | |
760 | ||
436dd70f | 761 | r = parse_chain(&t, false, &first); |
36697dc0 LP |
762 | if (r < 0) |
763 | return r; | |
764 | ||
765 | /* Already the end? A ':' as separator? In that case this was a time, not a date */ | |
fb3ba5ec | 766 | if (IN_SET(*t, 0, ':')) |
36697dc0 | 767 | return 0; |
36697dc0 | 768 | |
8ea80351 DC |
769 | if (*t == '~') |
770 | c->end_of_month = true; | |
fb3ba5ec | 771 | else if (*t != '-') |
36697dc0 | 772 | return -EINVAL; |
36697dc0 LP |
773 | |
774 | t++; | |
436dd70f | 775 | r = parse_chain(&t, false, &second); |
fb3ba5ec | 776 | if (r < 0) |
36697dc0 | 777 | return r; |
36697dc0 LP |
778 | |
779 | /* Got two parts, hence it's month and day */ | |
4c701096 | 780 | if (IN_SET(*t, 0, ' ')) { |
36697dc0 | 781 | *p = t + strspn(t, " "); |
fb3ba5ec YW |
782 | c->month = TAKE_PTR(first); |
783 | c->day = TAKE_PTR(second); | |
36697dc0 | 784 | return 0; |
fb3ba5ec | 785 | } else if (c->end_of_month) |
8ea80351 | 786 | return -EINVAL; |
36697dc0 | 787 | |
8ea80351 DC |
788 | if (*t == '~') |
789 | c->end_of_month = true; | |
fb3ba5ec | 790 | else if (*t != '-') |
36697dc0 | 791 | return -EINVAL; |
36697dc0 LP |
792 | |
793 | t++; | |
436dd70f | 794 | r = parse_chain(&t, false, &third); |
fb3ba5ec | 795 | if (r < 0) |
36697dc0 | 796 | return r; |
36697dc0 | 797 | |
fb3ba5ec YW |
798 | if (!IN_SET(*t, 0, ' ')) |
799 | return -EINVAL; | |
36697dc0 | 800 | |
fb3ba5ec YW |
801 | /* Got three parts, hence it is year, month and day */ |
802 | *p = t + strspn(t, " "); | |
803 | c->year = TAKE_PTR(first); | |
804 | c->month = TAKE_PTR(second); | |
805 | c->day = TAKE_PTR(third); | |
806 | return 0; | |
36697dc0 LP |
807 | } |
808 | ||
519cffec | 809 | static int parse_calendar_time(const char **p, CalendarSpec *c) { |
fb3ba5ec | 810 | _cleanup_(chain_freep) CalendarComponent *h = NULL, *m = NULL, *s = NULL; |
36697dc0 LP |
811 | const char *t; |
812 | int r; | |
813 | ||
814 | assert(p); | |
815 | assert(*p); | |
816 | assert(c); | |
817 | ||
818 | t = *p; | |
819 | ||
408a51e1 DC |
820 | /* If no time is specified at all, then this means 00:00:00 */ |
821 | if (*t == 0) | |
822 | goto null_hour; | |
36697dc0 | 823 | |
436dd70f | 824 | r = parse_chain(&t, false, &h); |
36697dc0 | 825 | if (r < 0) |
fb3ba5ec | 826 | return r; |
36697dc0 | 827 | |
fb3ba5ec YW |
828 | if (*t != ':') |
829 | return -EINVAL; | |
36697dc0 LP |
830 | |
831 | t++; | |
436dd70f | 832 | r = parse_chain(&t, false, &m); |
36697dc0 | 833 | if (r < 0) |
fb3ba5ec | 834 | return r; |
36697dc0 LP |
835 | |
836 | /* Already at the end? Then it's hours and minutes, and seconds are 0 */ | |
c0df71fa DC |
837 | if (*t == 0) |
838 | goto null_second; | |
36697dc0 | 839 | |
fb3ba5ec YW |
840 | if (*t != ':') |
841 | return -EINVAL; | |
36697dc0 LP |
842 | |
843 | t++; | |
436dd70f | 844 | r = parse_chain(&t, true, &s); |
36697dc0 | 845 | if (r < 0) |
fb3ba5ec | 846 | return r; |
36697dc0 LP |
847 | |
848 | /* At the end? Then it's hours, minutes and seconds */ | |
849 | if (*t == 0) | |
850 | goto finish; | |
851 | ||
fb3ba5ec | 852 | return -EINVAL; |
36697dc0 LP |
853 | |
854 | null_hour: | |
855 | r = const_chain(0, &h); | |
856 | if (r < 0) | |
fb3ba5ec | 857 | return r; |
36697dc0 LP |
858 | |
859 | r = const_chain(0, &m); | |
860 | if (r < 0) | |
fb3ba5ec | 861 | return r; |
36697dc0 LP |
862 | |
863 | null_second: | |
864 | r = const_chain(0, &s); | |
865 | if (r < 0) | |
fb3ba5ec | 866 | return r; |
36697dc0 LP |
867 | |
868 | finish: | |
869 | *p = t; | |
fb3ba5ec YW |
870 | c->hour = TAKE_PTR(h); |
871 | c->minute = TAKE_PTR(m); | |
872 | c->microsecond = TAKE_PTR(s); | |
436dd70f | 873 | |
36697dc0 | 874 | return 0; |
36697dc0 LP |
875 | } |
876 | ||
877 | int calendar_spec_from_string(const char *p, CalendarSpec **spec) { | |
21b3a0fc | 878 | const char *utc; |
921b5987 | 879 | _cleanup_(calendar_spec_freep) CalendarSpec *c = NULL; |
ea53cfd1 | 880 | _cleanup_free_ char *p_tmp = NULL; |
36697dc0 LP |
881 | int r; |
882 | ||
883 | assert(p); | |
36697dc0 | 884 | |
4122b14b | 885 | c = new(CalendarSpec, 1); |
36697dc0 LP |
886 | if (!c) |
887 | return -ENOMEM; | |
4122b14b YW |
888 | |
889 | *c = (CalendarSpec) { | |
890 | .dst = -1, | |
891 | .timezone = NULL, | |
892 | }; | |
36697dc0 | 893 | |
078efddd HV |
894 | utc = endswith_no_case(p, " UTC"); |
895 | if (utc) { | |
896 | c->utc = true; | |
ea53cfd1 ZJS |
897 | p = p_tmp = strndup(p, utc - p); |
898 | if (!p) | |
899 | return -ENOMEM; | |
21b3a0fc LP |
900 | } else { |
901 | const char *e = NULL; | |
902 | int j; | |
903 | ||
904 | tzset(); | |
905 | ||
906 | /* Check if the local timezone was specified? */ | |
907 | for (j = 0; j <= 1; j++) { | |
908 | if (isempty(tzname[j])) | |
909 | continue; | |
910 | ||
911 | e = endswith_no_case(p, tzname[j]); | |
d80e5b74 | 912 | if (!e) |
21b3a0fc LP |
913 | continue; |
914 | if (e == p) | |
915 | continue; | |
916 | if (e[-1] != ' ') | |
917 | continue; | |
918 | ||
919 | break; | |
920 | } | |
921 | ||
922 | /* Found one of the two timezones specified? */ | |
923 | if (IN_SET(j, 0, 1)) { | |
ea53cfd1 ZJS |
924 | p = p_tmp = strndup(p, e - p - 1); |
925 | if (!p) | |
926 | return -ENOMEM; | |
927 | ||
21b3a0fc | 928 | c->dst = j; |
48d26c01 IK |
929 | } else { |
930 | const char *last_space; | |
48d26c01 | 931 | |
a2932a0d | 932 | last_space = strrchr(p, ' '); |
089fb865 | 933 | if (last_space != NULL && timezone_is_valid(last_space + 1, LOG_DEBUG)) { |
a2932a0d | 934 | c->timezone = strdup(last_space + 1); |
921b5987 DT |
935 | if (!c->timezone) |
936 | return -ENOMEM; | |
a2932a0d | 937 | |
ea53cfd1 ZJS |
938 | p = p_tmp = strndup(p, last_space - p); |
939 | if (!p) | |
940 | return -ENOMEM; | |
48d26c01 | 941 | } |
21b3a0fc | 942 | } |
078efddd | 943 | } |
51ffa239 | 944 | |
921b5987 DT |
945 | if (isempty(p)) |
946 | return -EINVAL; | |
04773cb5 | 947 | |
272ac205 | 948 | if (strcaseeq(p, "minutely")) { |
436dd70f | 949 | r = const_chain(0, &c->microsecond); |
272ac205 | 950 | if (r < 0) |
921b5987 | 951 | return r; |
272ac205 DM |
952 | |
953 | } else if (strcaseeq(p, "hourly")) { | |
36697dc0 LP |
954 | r = const_chain(0, &c->minute); |
955 | if (r < 0) | |
921b5987 | 956 | return r; |
436dd70f | 957 | r = const_chain(0, &c->microsecond); |
36697dc0 | 958 | if (r < 0) |
921b5987 | 959 | return r; |
36697dc0 | 960 | |
b43d1d01 | 961 | } else if (strcaseeq(p, "daily")) { |
36697dc0 LP |
962 | r = const_chain(0, &c->hour); |
963 | if (r < 0) | |
921b5987 | 964 | return r; |
36697dc0 LP |
965 | r = const_chain(0, &c->minute); |
966 | if (r < 0) | |
921b5987 | 967 | return r; |
436dd70f | 968 | r = const_chain(0, &c->microsecond); |
36697dc0 | 969 | if (r < 0) |
921b5987 | 970 | return r; |
36697dc0 | 971 | |
b43d1d01 | 972 | } else if (strcaseeq(p, "monthly")) { |
36697dc0 LP |
973 | r = const_chain(1, &c->day); |
974 | if (r < 0) | |
921b5987 | 975 | return r; |
36697dc0 LP |
976 | r = const_chain(0, &c->hour); |
977 | if (r < 0) | |
921b5987 | 978 | return r; |
36697dc0 LP |
979 | r = const_chain(0, &c->minute); |
980 | if (r < 0) | |
921b5987 | 981 | return r; |
436dd70f | 982 | r = const_chain(0, &c->microsecond); |
36697dc0 | 983 | if (r < 0) |
921b5987 | 984 | return r; |
36697dc0 | 985 | |
ddd6a22a LP |
986 | } else if (STRCASE_IN_SET(p, |
987 | "annually", | |
988 | "yearly", | |
989 | "anually") /* backwards compatibility */ ) { | |
dbfd41e2 | 990 | |
13516818 LP |
991 | r = const_chain(1, &c->month); |
992 | if (r < 0) | |
921b5987 | 993 | return r; |
13516818 LP |
994 | r = const_chain(1, &c->day); |
995 | if (r < 0) | |
921b5987 | 996 | return r; |
13516818 LP |
997 | r = const_chain(0, &c->hour); |
998 | if (r < 0) | |
921b5987 | 999 | return r; |
13516818 LP |
1000 | r = const_chain(0, &c->minute); |
1001 | if (r < 0) | |
921b5987 | 1002 | return r; |
436dd70f | 1003 | r = const_chain(0, &c->microsecond); |
13516818 | 1004 | if (r < 0) |
921b5987 | 1005 | return r; |
13516818 | 1006 | |
b43d1d01 | 1007 | } else if (strcaseeq(p, "weekly")) { |
36697dc0 LP |
1008 | |
1009 | c->weekdays_bits = 1; | |
1010 | ||
1011 | r = const_chain(0, &c->hour); | |
1012 | if (r < 0) | |
921b5987 | 1013 | return r; |
36697dc0 LP |
1014 | r = const_chain(0, &c->minute); |
1015 | if (r < 0) | |
921b5987 | 1016 | return r; |
436dd70f | 1017 | r = const_chain(0, &c->microsecond); |
dbfd41e2 | 1018 | if (r < 0) |
921b5987 | 1019 | return r; |
dbfd41e2 LP |
1020 | |
1021 | } else if (strcaseeq(p, "quarterly")) { | |
1022 | ||
1023 | r = const_chain(1, &c->month); | |
1024 | if (r < 0) | |
921b5987 | 1025 | return r; |
dbfd41e2 LP |
1026 | r = const_chain(4, &c->month); |
1027 | if (r < 0) | |
921b5987 | 1028 | return r; |
dbfd41e2 LP |
1029 | r = const_chain(7, &c->month); |
1030 | if (r < 0) | |
921b5987 | 1031 | return r; |
dbfd41e2 LP |
1032 | r = const_chain(10, &c->month); |
1033 | if (r < 0) | |
921b5987 | 1034 | return r; |
dbfd41e2 LP |
1035 | r = const_chain(1, &c->day); |
1036 | if (r < 0) | |
921b5987 | 1037 | return r; |
dbfd41e2 LP |
1038 | r = const_chain(0, &c->hour); |
1039 | if (r < 0) | |
921b5987 | 1040 | return r; |
dbfd41e2 LP |
1041 | r = const_chain(0, &c->minute); |
1042 | if (r < 0) | |
921b5987 | 1043 | return r; |
436dd70f | 1044 | r = const_chain(0, &c->microsecond); |
dbfd41e2 | 1045 | if (r < 0) |
921b5987 | 1046 | return r; |
dbfd41e2 | 1047 | |
ddd6a22a LP |
1048 | } else if (STRCASE_IN_SET(p, |
1049 | "biannually", | |
1050 | "bi-annually", | |
1051 | "semiannually", | |
1052 | "semi-annually")) { | |
dbfd41e2 LP |
1053 | |
1054 | r = const_chain(1, &c->month); | |
1055 | if (r < 0) | |
921b5987 | 1056 | return r; |
dbfd41e2 LP |
1057 | r = const_chain(7, &c->month); |
1058 | if (r < 0) | |
921b5987 | 1059 | return r; |
dbfd41e2 LP |
1060 | r = const_chain(1, &c->day); |
1061 | if (r < 0) | |
921b5987 | 1062 | return r; |
dbfd41e2 LP |
1063 | r = const_chain(0, &c->hour); |
1064 | if (r < 0) | |
921b5987 | 1065 | return r; |
dbfd41e2 LP |
1066 | r = const_chain(0, &c->minute); |
1067 | if (r < 0) | |
921b5987 | 1068 | return r; |
436dd70f | 1069 | r = const_chain(0, &c->microsecond); |
36697dc0 | 1070 | if (r < 0) |
921b5987 | 1071 | return r; |
36697dc0 LP |
1072 | |
1073 | } else { | |
1074 | r = parse_weekdays(&p, c); | |
1075 | if (r < 0) | |
921b5987 | 1076 | return r; |
36697dc0 LP |
1077 | |
1078 | r = parse_date(&p, c); | |
1079 | if (r < 0) | |
921b5987 | 1080 | return r; |
36697dc0 | 1081 | |
d80e5b74 ZJS |
1082 | if (r == 0) { |
1083 | r = parse_calendar_time(&p, c); | |
1084 | if (r < 0) | |
921b5987 | 1085 | return r; |
d80e5b74 | 1086 | } |
36697dc0 | 1087 | |
921b5987 DT |
1088 | if (*p != 0) |
1089 | return -EINVAL; | |
36697dc0 LP |
1090 | } |
1091 | ||
1092 | r = calendar_spec_normalize(c); | |
1093 | if (r < 0) | |
921b5987 | 1094 | return r; |
36697dc0 | 1095 | |
921b5987 DT |
1096 | if (!calendar_spec_valid(c)) |
1097 | return -EINVAL; | |
36697dc0 | 1098 | |
9a2b3d3d ZJS |
1099 | if (spec) |
1100 | *spec = TAKE_PTR(c); | |
36697dc0 | 1101 | return 0; |
36697dc0 LP |
1102 | } |
1103 | ||
462f15d9 | 1104 | static int find_end_of_month(const struct tm *tm, bool utc, int day) { |
a2eb5ea7 DC |
1105 | struct tm t = *tm; |
1106 | ||
1107 | t.tm_mon++; | |
1108 | t.tm_mday = 1 - day; | |
1109 | ||
c477ff14 | 1110 | if (mktime_or_timegm(&t, utc) < 0 || |
a2eb5ea7 DC |
1111 | t.tm_mon != tm->tm_mon) |
1112 | return -1; | |
1113 | ||
1114 | return t.tm_mday; | |
1115 | } | |
1116 | ||
462f15d9 ZJS |
1117 | static int find_matching_component( |
1118 | const CalendarSpec *spec, | |
1119 | const CalendarComponent *c, | |
1120 | const struct tm *tm, /* tm is only used for end-of-month calculations */ | |
1121 | int *val) { | |
1122 | ||
1123 | int d = -1, r; | |
36697dc0 | 1124 | bool d_set = false; |
36697dc0 LP |
1125 | |
1126 | assert(val); | |
1127 | ||
462f15d9 ZJS |
1128 | /* Finds the *earliest* matching time specified by one of the CalendarCompoment items in chain c. |
1129 | * If no matches can be found, returns -ENOENT. | |
1130 | * Otherwise, updates *val to the matching time. 1 is returned if *val was changed, 0 otherwise. | |
1131 | */ | |
1132 | ||
36697dc0 LP |
1133 | if (!c) |
1134 | return 0; | |
1135 | ||
462f15d9 ZJS |
1136 | bool end_of_month = spec->end_of_month && c == spec->day; |
1137 | ||
36697dc0 | 1138 | while (c) { |
462f15d9 | 1139 | int start, stop; |
a2eb5ea7 | 1140 | |
462f15d9 ZJS |
1141 | if (end_of_month) { |
1142 | start = find_end_of_month(tm, spec->utc, c->start); | |
1143 | stop = find_end_of_month(tm, spec->utc, c->stop); | |
a2eb5ea7 | 1144 | |
e78fce48 DC |
1145 | if (stop > 0) |
1146 | SWAP_TWO(start, stop); | |
462f15d9 ZJS |
1147 | } else { |
1148 | start = c->start; | |
1149 | stop = c->stop; | |
a2eb5ea7 | 1150 | } |
8ea80351 | 1151 | |
e78fce48 | 1152 | if (start >= *val) { |
36697dc0 | 1153 | |
e78fce48 DC |
1154 | if (!d_set || start < d) { |
1155 | d = start; | |
36697dc0 LP |
1156 | d_set = true; |
1157 | } | |
1158 | ||
1159 | } else if (c->repeat > 0) { | |
1160 | int k; | |
1161 | ||
be6b0c21 | 1162 | k = start + c->repeat * DIV_ROUND_UP(*val - start, c->repeat); |
36697dc0 | 1163 | |
e78fce48 | 1164 | if ((!d_set || k < d) && (stop < 0 || k <= stop)) { |
36697dc0 LP |
1165 | d = k; |
1166 | d_set = true; | |
1167 | } | |
1168 | } | |
1169 | ||
482f3b54 | 1170 | c = c->next; |
36697dc0 LP |
1171 | } |
1172 | ||
1173 | if (!d_set) | |
1174 | return -ENOENT; | |
1175 | ||
1176 | r = *val != d; | |
1177 | *val = d; | |
1178 | return r; | |
1179 | } | |
1180 | ||
be75c86d | 1181 | static int tm_within_bounds(struct tm *tm, bool utc) { |
36697dc0 LP |
1182 | struct tm t; |
1183 | assert(tm); | |
1184 | ||
f6e7d66b DC |
1185 | /* |
1186 | * Set an upper bound on the year so impossible dates like "*-02-31" | |
1187 | * don't cause find_next() to loop forever. tm_year contains years | |
1188 | * since 1900, so adjust it accordingly. | |
1189 | */ | |
1190 | if (tm->tm_year + 1900 > MAX_YEAR) | |
be75c86d ZJS |
1191 | return -ERANGE; |
1192 | ||
1193 | t = *tm; | |
1194 | if (mktime_or_timegm(&t, utc) < 0) | |
1195 | return negative_errno(); | |
f6e7d66b | 1196 | |
36697dc0 | 1197 | /* Did any normalization take place? If so, it was out of bounds before */ |
129cb6e2 ZJS |
1198 | int cmp = CMP(t.tm_year, tm->tm_year) ?: |
1199 | CMP(t.tm_mon, tm->tm_mon) ?: | |
1200 | CMP(t.tm_mday, tm->tm_mday) ?: | |
1201 | CMP(t.tm_hour, tm->tm_hour) ?: | |
1202 | CMP(t.tm_min, tm->tm_min) ?: | |
1203 | CMP(t.tm_sec, tm->tm_sec); | |
1204 | ||
1205 | if (cmp < 0) | |
1206 | return -EDEADLK; /* Refuse to go backward */ | |
1207 | if (cmp > 0) | |
be75c86d | 1208 | *tm = t; |
129cb6e2 | 1209 | return cmp == 0; |
36697dc0 LP |
1210 | } |
1211 | ||
51ffa239 | 1212 | static bool matches_weekday(int weekdays_bits, const struct tm *tm, bool utc) { |
36697dc0 LP |
1213 | struct tm t; |
1214 | int k; | |
1215 | ||
489464d0 | 1216 | if (weekdays_bits < 0 || weekdays_bits >= BITS_WEEKDAYS) |
36697dc0 LP |
1217 | return true; |
1218 | ||
1219 | t = *tm; | |
c477ff14 | 1220 | if (mktime_or_timegm(&t, utc) < 0) |
36697dc0 LP |
1221 | return false; |
1222 | ||
1223 | k = t.tm_wday == 0 ? 6 : t.tm_wday - 1; | |
1224 | return (weekdays_bits & (1 << k)); | |
1225 | } | |
1226 | ||
169615c9 ZJS |
1227 | /* A safety valve: if we get stuck in the calculation, return an error. |
1228 | * C.f. https://bugzilla.redhat.com/show_bug.cgi?id=1941335. */ | |
1229 | #define MAX_CALENDAR_ITERATIONS 1000 | |
1230 | ||
436dd70f | 1231 | static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) { |
36697dc0 | 1232 | struct tm c; |
436dd70f | 1233 | int tm_usec; |
36697dc0 LP |
1234 | int r; |
1235 | ||
9bb656bd LP |
1236 | /* Returns -ENOENT if the expression is not going to elapse anymore */ |
1237 | ||
36697dc0 LP |
1238 | assert(spec); |
1239 | assert(tm); | |
1240 | ||
1241 | c = *tm; | |
436dd70f | 1242 | tm_usec = *usec; |
36697dc0 | 1243 | |
169615c9 | 1244 | for (unsigned iteration = 0; iteration < MAX_CALENDAR_ITERATIONS; iteration++) { |
36697dc0 | 1245 | /* Normalize the current date */ |
ea3894c1 | 1246 | (void) mktime_or_timegm(&c, spec->utc); |
21b3a0fc | 1247 | c.tm_isdst = spec->dst; |
36697dc0 LP |
1248 | |
1249 | c.tm_year += 1900; | |
8ea80351 | 1250 | r = find_matching_component(spec, spec->year, &c, &c.tm_year); |
36697dc0 LP |
1251 | c.tm_year -= 1900; |
1252 | ||
1253 | if (r > 0) { | |
1254 | c.tm_mon = 0; | |
1255 | c.tm_mday = 1; | |
436dd70f | 1256 | c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0; |
36697dc0 | 1257 | } |
e308ddca | 1258 | if (r < 0) |
36697dc0 | 1259 | return r; |
be75c86d | 1260 | if (tm_within_bounds(&c, spec->utc) <= 0) |
e308ddca | 1261 | return -ENOENT; |
36697dc0 LP |
1262 | |
1263 | c.tm_mon += 1; | |
8ea80351 | 1264 | r = find_matching_component(spec, spec->month, &c, &c.tm_mon); |
36697dc0 LP |
1265 | c.tm_mon -= 1; |
1266 | ||
1267 | if (r > 0) { | |
1268 | c.tm_mday = 1; | |
436dd70f | 1269 | c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0; |
36697dc0 | 1270 | } |
be75c86d | 1271 | if (r < 0 || (r = tm_within_bounds(&c, spec->utc)) < 0) { |
313cefa1 | 1272 | c.tm_year++; |
36697dc0 LP |
1273 | c.tm_mon = 0; |
1274 | c.tm_mday = 1; | |
436dd70f | 1275 | c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0; |
36697dc0 LP |
1276 | continue; |
1277 | } | |
be75c86d ZJS |
1278 | if (r == 0) |
1279 | continue; | |
36697dc0 | 1280 | |
8ea80351 | 1281 | r = find_matching_component(spec, spec->day, &c, &c.tm_mday); |
36697dc0 | 1282 | if (r > 0) |
436dd70f | 1283 | c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0; |
be75c86d | 1284 | if (r < 0 || (r = tm_within_bounds(&c, spec->utc)) < 0) { |
313cefa1 | 1285 | c.tm_mon++; |
36697dc0 | 1286 | c.tm_mday = 1; |
436dd70f | 1287 | c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0; |
36697dc0 LP |
1288 | continue; |
1289 | } | |
be75c86d ZJS |
1290 | if (r == 0) |
1291 | continue; | |
36697dc0 | 1292 | |
51ffa239 | 1293 | if (!matches_weekday(spec->weekdays_bits, &c, spec->utc)) { |
36697dc0 | 1294 | c.tm_mday++; |
436dd70f | 1295 | c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0; |
36697dc0 LP |
1296 | continue; |
1297 | } | |
1298 | ||
8ea80351 | 1299 | r = find_matching_component(spec, spec->hour, &c, &c.tm_hour); |
36697dc0 | 1300 | if (r > 0) |
a022d76e | 1301 | c.tm_min = c.tm_sec = tm_usec = 0; |
be75c86d | 1302 | if (r < 0 || (r = tm_within_bounds(&c, spec->utc)) < 0) { |
313cefa1 | 1303 | c.tm_mday++; |
436dd70f | 1304 | c.tm_hour = c.tm_min = c.tm_sec = tm_usec = 0; |
36697dc0 LP |
1305 | continue; |
1306 | } | |
be75c86d ZJS |
1307 | if (r == 0) |
1308 | /* The next hour we set might be missing if there | |
1309 | * are time zone changes. Let's try again starting at | |
1310 | * normalized time. */ | |
1311 | continue; | |
36697dc0 | 1312 | |
8ea80351 | 1313 | r = find_matching_component(spec, spec->minute, &c, &c.tm_min); |
36697dc0 | 1314 | if (r > 0) |
a022d76e | 1315 | c.tm_sec = tm_usec = 0; |
be75c86d | 1316 | if (r < 0 || (r = tm_within_bounds(&c, spec->utc)) < 0) { |
313cefa1 | 1317 | c.tm_hour++; |
436dd70f | 1318 | c.tm_min = c.tm_sec = tm_usec = 0; |
36697dc0 LP |
1319 | continue; |
1320 | } | |
be75c86d ZJS |
1321 | if (r == 0) |
1322 | continue; | |
36697dc0 | 1323 | |
436dd70f | 1324 | c.tm_sec = c.tm_sec * USEC_PER_SEC + tm_usec; |
8ea80351 | 1325 | r = find_matching_component(spec, spec->microsecond, &c, &c.tm_sec); |
436dd70f HV |
1326 | tm_usec = c.tm_sec % USEC_PER_SEC; |
1327 | c.tm_sec /= USEC_PER_SEC; | |
1328 | ||
be75c86d | 1329 | if (r < 0 || (r = tm_within_bounds(&c, spec->utc)) < 0) { |
313cefa1 | 1330 | c.tm_min++; |
436dd70f | 1331 | c.tm_sec = tm_usec = 0; |
36697dc0 LP |
1332 | continue; |
1333 | } | |
be75c86d ZJS |
1334 | if (r == 0) |
1335 | continue; | |
36697dc0 | 1336 | |
36697dc0 | 1337 | *tm = c; |
436dd70f | 1338 | *usec = tm_usec; |
36697dc0 LP |
1339 | return 0; |
1340 | } | |
169615c9 ZJS |
1341 | |
1342 | /* It seems we entered an infinite loop. Let's gracefully return an error instead of hanging or | |
1343 | * aborting. This code is also exercised when timers.target is brought up during early boot, so | |
1344 | * aborting here is problematic and hard to diagnose for users. */ | |
1345 | _cleanup_free_ char *s = NULL; | |
1346 | (void) calendar_spec_to_string(spec, &s); | |
1347 | return log_warning_errno(SYNTHETIC_ERRNO(EDEADLK), | |
1348 | "Infinite loop in calendar calculation: %s", strna(s)); | |
36697dc0 LP |
1349 | } |
1350 | ||
9bb656bd | 1351 | static int calendar_spec_next_usec_impl(const CalendarSpec *spec, usec_t usec, usec_t *ret_next) { |
36697dc0 LP |
1352 | struct tm tm; |
1353 | time_t t; | |
1354 | int r; | |
436dd70f | 1355 | usec_t tm_usec; |
36697dc0 LP |
1356 | |
1357 | assert(spec); | |
36697dc0 | 1358 | |
1bb4b028 LP |
1359 | if (usec > USEC_TIMESTAMP_FORMATTABLE_MAX) |
1360 | return -EINVAL; | |
1361 | ||
436dd70f HV |
1362 | usec++; |
1363 | t = (time_t) (usec / USEC_PER_SEC); | |
51ffa239 | 1364 | assert_se(localtime_or_gmtime_r(&t, &tm, spec->utc)); |
436dd70f | 1365 | tm_usec = usec % USEC_PER_SEC; |
36697dc0 | 1366 | |
436dd70f | 1367 | r = find_next(spec, &tm, &tm_usec); |
36697dc0 LP |
1368 | if (r < 0) |
1369 | return r; | |
1370 | ||
51ffa239 | 1371 | t = mktime_or_timegm(&tm, spec->utc); |
c477ff14 | 1372 | if (t < 0) |
36697dc0 LP |
1373 | return -EINVAL; |
1374 | ||
9bb656bd LP |
1375 | if (ret_next) |
1376 | *ret_next = (usec_t) t * USEC_PER_SEC + tm_usec; | |
1377 | ||
36697dc0 LP |
1378 | return 0; |
1379 | } | |
48d26c01 IK |
1380 | |
1381 | typedef struct SpecNextResult { | |
1382 | usec_t next; | |
1383 | int return_value; | |
1384 | } SpecNextResult; | |
1385 | ||
9bb656bd | 1386 | int calendar_spec_next_usec(const CalendarSpec *spec, usec_t usec, usec_t *ret_next) { |
4c253ed1 | 1387 | SpecNextResult *shared, tmp; |
48d26c01 IK |
1388 | int r; |
1389 | ||
9bb656bd LP |
1390 | assert(spec); |
1391 | ||
48d26c01 | 1392 | if (isempty(spec->timezone)) |
9bb656bd | 1393 | return calendar_spec_next_usec_impl(spec, usec, ret_next); |
48d26c01 IK |
1394 | |
1395 | shared = mmap(NULL, sizeof *shared, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); | |
1396 | if (shared == MAP_FAILED) | |
1397 | return negative_errno(); | |
1398 | ||
1f5d1e02 | 1399 | r = safe_fork("(sd-calendar)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_WAIT, NULL); |
4c253ed1 | 1400 | if (r < 0) { |
48d26c01 | 1401 | (void) munmap(shared, sizeof *shared); |
4c253ed1 | 1402 | return r; |
48d26c01 | 1403 | } |
4c253ed1 | 1404 | if (r == 0) { |
437f48a4 LP |
1405 | char *colon_tz; |
1406 | ||
1407 | /* tzset(3) says $TZ should be prefixed with ":" if we reference timezone files */ | |
1408 | colon_tz = strjoina(":", spec->timezone); | |
1409 | ||
1410 | if (setenv("TZ", colon_tz, 1) != 0) { | |
48d26c01 IK |
1411 | shared->return_value = negative_errno(); |
1412 | _exit(EXIT_FAILURE); | |
1413 | } | |
1414 | ||
1415 | tzset(); | |
1416 | ||
1417 | shared->return_value = calendar_spec_next_usec_impl(spec, usec, &shared->next); | |
1418 | ||
1419 | _exit(EXIT_SUCCESS); | |
1420 | } | |
1421 | ||
48d26c01 | 1422 | tmp = *shared; |
1f5d1e02 | 1423 | if (munmap(shared, sizeof *shared) < 0) |
48d26c01 IK |
1424 | return negative_errno(); |
1425 | ||
9bb656bd LP |
1426 | if (tmp.return_value == 0 && ret_next) |
1427 | *ret_next = tmp.next; | |
48d26c01 IK |
1428 | |
1429 | return tmp.return_value; | |
1430 | } |