1 /*
2  *  Tvheadend - cron routines
3  *
4  *  Copyright (C) 2014 Adam Sutton
5  *
6  *  This program is free software: you can redistribute it and/or modify
7  *  it under the terms of the GNU General Public License as published by
8  *  the Free Software Foundation, either version 3 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "build.h"
21 #include "cron.h"
22 #include "tvheadend.h"
23 
24 #include <time.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 
29 #if !defined(PLATFORM_FREEBSD)
30 #include <alloca.h>
31 #endif
32 
33 /*
34  * Parse value
35  */
36 static int
cron_parse_val(const char * str,const char ** key,int * v)37 cron_parse_val ( const char *str, const char **key, int *v )
38 {
39   int i = 0;
40   if (!str)
41     return 0;
42   if (key) {
43     while (key[i]) {
44       if (!strncasecmp(str, key[i], strlen(key[i]))) {
45         *v = i;
46         return 0;
47       }
48       i++;
49     }
50   }
51 
52   return sscanf(str, "%d", v) == 1 ? 0 : 1;
53 }
54 
55 /*
56  * Parse individual field in cron spec
57  */
58 static int
cron_parse_field(const char ** istr,uint64_t * field,uint64_t mask,int bits,int off,const char ** key)59 cron_parse_field
60   ( const char **istr, uint64_t *field, uint64_t mask, int bits, int off,
61     const char **key )
62 {
63   int sn = -1, en = -1, mn = -1;
64   const char *str = *istr;
65   const char *beg = str;
66   uint64_t    val = 0;
67   while ( 1 ) {
68     if ( *str == '*' ) {
69       sn     = off;
70       en     = bits + off - 1;
71       beg    = NULL;
72     } else if ( *str == ',' || *str == ' ' || *str == '\0' ) {
73       if (beg)
74         if (cron_parse_val(beg, key, en == -1 ? (sn == -1 ? &sn : &en) : &mn))
75           return 1;
76       if ((sn - off) >= bits || (en - off) >= bits || mn > bits)
77         return 1;
78       if (en < 0) en = sn;
79       if (mn <= 0) mn = 1;
80       while (sn <= en) {
81         if ( (sn % mn) == 0 )
82           val |= (0x1ULL << (sn - off));
83         sn++;
84       }
85       if (*str != ',') break;
86       sn = en = mn = -1;
87       beg = (str + 1);
88     } else if ( *str == '/' ) {
89       if (beg)
90         if (en == -1 || cron_parse_val(beg, key, sn == -1 ? &sn : &en))
91           return 1;
92       beg = (str + 1);
93     } else if ( *str == '-' ) {
94       if (sn != -1 || cron_parse_val(beg, key, &sn))
95         return 1;
96       beg = (str + 1);
97     }
98     str++;
99   }
100   if (*str == ' ') str++;
101   *istr   = str;
102   *field  = (val | ((val >> bits) & 0x1)) & mask;
103   return 0;
104 }
105 
106 /*
107  * Set value
108  */
109 int
cron_set(cron_t * c,const char * str)110 cron_set ( cron_t *c, const char *str )
111 {
112   uint64_t ho, mi, mo, dm, dw;
113   static const char *days[] = {
114     "sun", "mon", "tue", "wed", "thu", "fri", "sat", NULL
115   };
116   static const char *months[] = {
117     "ignore",
118     "jan", "feb", "mar", "apr", "may", "jun",
119     "jul", "aug", "sep", "oct", "nov", "dec",
120     NULL
121   };
122 
123   /* Daily (01:01) */
124   if ( !strcmp(str, "@daily") ) {
125     c->c_min  = 1;
126     c->c_hour = 1;
127     c->c_mday = CRON_MDAY_MASK;
128     c->c_mon  = CRON_MON_MASK;
129     c->c_wday = CRON_WDAY_MASK;
130 
131   /* Hourly (XX:02) */
132   } else if ( !strcmp(str, "@hourly") ) {
133     c->c_min  = 2;
134     c->c_hour = CRON_HOUR_MASK;
135     c->c_mday = CRON_MDAY_MASK;
136     c->c_mon  = CRON_MON_MASK;
137     c->c_wday = CRON_WDAY_MASK;
138 
139   /* Standard */
140   } else {
141     if (cron_parse_field(&str, &mi, CRON_MIN_MASK,  60, 0, NULL)   || !mi)
142       return 1;
143     if (cron_parse_field(&str, &ho, CRON_HOUR_MASK, 24, 0, NULL)   || !ho)
144       return 1;
145     if (cron_parse_field(&str, &dm, CRON_MDAY_MASK, 31, 1, NULL)   || !dm)
146       return 1;
147     if (cron_parse_field(&str, &mo, CRON_MON_MASK,  12, 1, months) || !mo)
148       return 1;
149     if (cron_parse_field(&str, &dw, CRON_WDAY_MASK, 7,  0, days)   || !dw)
150       return 1;
151     c->c_min  = mi;
152     c->c_hour = ho;
153     c->c_mday = dm;
154     c->c_mon  = mo;
155     c->c_wday = dw;
156   }
157 
158   return 0;
159 }
160 
161 /*
162  * Set value
163  */
164 cron_multi_t *
cron_multi_set(const char * str)165 cron_multi_set ( const char *str )
166 {
167   char *s = str ? alloca(strlen(str) + 1) : NULL;
168   char *line, *sptr = NULL;
169   cron_t cron;
170   cron_multi_t *cm = NULL, *cm2;
171   int count = 0;
172 
173   if (s == NULL)
174     return NULL;
175   strcpy(s, str);
176   line = strtok_r(s, "\n", &sptr);
177   while (line) {
178     while (*line && *line <= ' ')
179       line++;
180     if (line[0] != '#')
181       if (!cron_set(&cron, line)) {
182         count++;
183         cm2 = realloc(cm, sizeof(*cm) + sizeof(cron) * count);
184         if (cm2 == NULL) {
185           free(cm);
186           return NULL;
187         }
188         cm = cm2;
189         cm->cm_crons[count - 1] = cron;
190       }
191     line = strtok_r(NULL, "\n", &sptr);
192   }
193   if (count)
194     cm->cm_count = count;
195   return cm;
196 }
197 
198 /*
199  * Check for leap year
200  */
201 static int
is_leep_year(int year)202 is_leep_year ( int year )
203 {
204   if (!(year % 400))
205     return 1;
206   if (!(year % 100))
207     return 0;
208   return (year % 4) ? 0 : 1;
209 }
210 
211 /*
212  * Check for days in month
213  */
214 static int
days_in_month(int year,int mon)215 days_in_month ( int year, int mon )
216 {
217   int d;
218   if (mon == 2)
219     d = 28 + is_leep_year(year);
220   else
221     d = 30 + ((0x15AA >> mon) & 0x1);
222   return d;
223 }
224 
225 /*
226  * Find the next time (starting from now) that the cron should fire
227  */
228 int
cron_next(cron_t * c,const time_t now,time_t * ret)229 cron_next ( cron_t *c, const time_t now, time_t *ret )
230 {
231   struct tm nxt, tmp;
232   int endyear, loops = 1000;
233   localtime_r(&now, &nxt);
234   endyear = nxt.tm_year + 10;
235 
236   /* Clear seconds */
237   nxt.tm_sec = 0;
238 
239   /* Invalid day */
240   if (!(c->c_mday & (0x1LL << (nxt.tm_mday-1))) ||
241       !(c->c_wday & (0x1LL << (nxt.tm_wday)))   ||
242       !(c->c_mon & (0x1LL << (nxt.tm_mon))) ) {
243     nxt.tm_min  = 0;
244     nxt.tm_hour = 0;
245 
246   /* Invalid hour */
247   } else if (!(c->c_hour & (0x1LL << nxt.tm_hour))) {
248     nxt.tm_min  = 0;
249 
250   /* Increment */
251   } else {
252     ++nxt.tm_min;
253   }
254 
255   /* Minute */
256   while (!(c->c_min & (0x1LL << nxt.tm_min))) {
257     if (nxt.tm_min == 60) {
258       ++nxt.tm_hour;
259       nxt.tm_min = 0;
260     } else
261       nxt.tm_min++;
262   }
263 
264   /* Hour */
265   while (!(c->c_hour & (0x1LL << nxt.tm_hour))) {
266     if (nxt.tm_hour == 24) {
267       ++nxt.tm_mday;
268       ++nxt.tm_wday;
269       nxt.tm_hour = 0;
270     } else
271       ++nxt.tm_hour;
272   }
273 
274   /* Date */
275   if (nxt.tm_wday == 7)
276     nxt.tm_wday = 0;
277   if (nxt.tm_mday > days_in_month(nxt.tm_year+1900, nxt.tm_mon+1)) {
278     nxt.tm_mday = 1;
279     nxt.tm_mon++;
280     if (nxt.tm_mon == 12) {
281       nxt.tm_mon = 0;
282       ++nxt.tm_year;
283     }
284   }
285   while (!(c->c_mday & (0x1LL << (nxt.tm_mday-1))) ||
286          !(c->c_wday & (0x1LL << (nxt.tm_wday)))   ||
287          !(c->c_mon & (0x1LL << (nxt.tm_mon))) ) {
288 
289     /* Endless loop protection */
290     if (loops-- == 0)
291       return -1;
292 
293     /* Stop possible infinite loop on invalid request */
294     if (nxt.tm_year >= endyear)
295       return -1;
296 
297     /* Increment day of week */
298     if (++nxt.tm_wday == 7)
299       nxt.tm_wday = 0;
300 
301     /* Increment day */
302     if (++nxt.tm_mday > days_in_month(nxt.tm_year+1900, nxt.tm_mon+1)) {
303       nxt.tm_mday = 1;
304       if (++nxt.tm_mon == 12) {
305         nxt.tm_mon = 0;
306         ++nxt.tm_year;
307       }
308     }
309 
310     /* Shortcut the month */
311     while (!(c->c_mon & (0x1LL << nxt.tm_mon))) {
312       nxt.tm_wday
313         += 1 + (days_in_month(nxt.tm_year+1900, nxt.tm_mon+1) - nxt.tm_mday);
314       nxt.tm_mday = 1;
315       if (++nxt.tm_mon >= 12) {
316         nxt.tm_mon = 0;
317         ++nxt.tm_year;
318       }
319     }
320     nxt.tm_wday %= 7;
321   }
322 
323   /* Create time */
324   memcpy(&tmp, &nxt, sizeof(tmp));
325   mktime(&tmp);
326   nxt.tm_isdst = tmp.tm_isdst;
327   *ret         = mktime(&nxt);
328   if (*ret <= now)
329     *ret = mktime(&tmp);
330   if (*ret <= now) {
331 #ifndef CRON_TEST
332     tvherror(LS_CRON, "invalid time, now %"PRItime_t", result %"PRItime_t, now, *ret);
333 #else
334     printf("ERROR: invalid time, now %"PRItime_t", result %"PRItime_t"\n", now, *ret);
335 #endif
336     *ret = now + 600;
337   }
338   return 0;
339 }
340 
341 /*
342  * Find the next time (starting from now) that the cron should fire
343  */
344 int
cron_multi_next(cron_multi_t * cm,const time_t now,time_t * ret)345 cron_multi_next ( cron_multi_t *cm, const time_t now, time_t *ret )
346 {
347   uint32_t i;
348   time_t r = (time_t)-1, t;
349 
350   if (cm == NULL)
351     return -1;
352   for (i = 0; i < cm->cm_count; i++)
353     if (!cron_next(&cm->cm_crons[i], now, &t))
354       if (r == (time_t)-1 || t < r)
355         r = t;
356   if (r == (time_t)-1)
357     return -1;
358   *ret = r;
359   return 0;
360 }
361 
362 /*
363  * Testing
364  *
365  *   gcc -g -DCRON_TEST -I./build.linux src/cron.c
366  */
367 #ifdef CRON_TEST
368 static
print_bits(uint64_t b,int n)369 void print_bits ( uint64_t b, int n )
370 {
371   while (n) {
372     printf("%d", (int)(b & 0x1));
373     b >>= 1;
374     n--;
375   }
376 }
377 
378 int
main(int argc,char ** argv)379 main ( int argc, char **argv )
380 {
381   cron_t c;
382   time_t n;
383   struct tm tm;
384   char buf[128];
385 
386   if (argc < 2) {
387     printf("Specify: CRON [NOW]\n");
388     return 1;
389   }
390   if (argc > 2)
391     n = atol(argv[2]);
392   else
393     time(&n);
394   if (cron_set(&c, argv[1]))
395     printf("INVALID CRON: %s\n", argv[1]);
396   else {
397     printf("min  = "); print_bits(c.c_min,  60); printf("\n");
398     printf("hour = "); print_bits(c.c_hour, 24); printf("\n");
399     printf("mday = "); print_bits(c.c_mday, 31); printf("\n");
400     printf("mon  = "); print_bits(c.c_mon,  12); printf("\n");
401     printf("wday = "); print_bits(c.c_wday,  7); printf("\n");
402 
403     localtime_r(&n, &tm);
404     strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &tm);
405     printf("NOW: %ld - %s (DST %d) (ZONE %s)\n", (long)n, buf, tm.tm_isdst, tm.tm_zone);
406 
407     if (cron_next(&c, n, &n)) {
408       printf("FAILED to find NEXT\n");
409       return 1;
410     }
411     localtime_r(&n, &tm);
412     strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &tm);
413     printf("NXT: %ld - %s (DST %d) (ZONE %s)\n", (long)n, buf, tm.tm_isdst, tm.tm_zone);
414 
415   }
416   return 0;
417 }
418 #endif
419 
420 /******************************************************************************
421  * Editor Configuration
422  *
423  * vim:sts=2:ts=2:sw=2:et
424  *****************************************************************************/
425