1 /* Parse a time duration and return a seconds count
2 Copyright (C) 2008-2015 Free Software Foundation, Inc.
3 Written by Bruce Korb <[email protected]>, 2008.
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>. */
17
18 #include <config.h>
19
20 /* Specification. */
21 #include "parse-duration.h"
22
23 #include <ctype.h>
24 #include <errno.h>
25 #include <limits.h>
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
29
30 #include "intprops.h"
31
32 #ifndef NUL
33 #define NUL '\0'
34 #endif
35
36 #define cch_t char const
37
38 typedef enum {
39 NOTHING_IS_DONE,
40 YEAR_IS_DONE,
41 MONTH_IS_DONE,
42 WEEK_IS_DONE,
43 DAY_IS_DONE,
44 HOUR_IS_DONE,
45 MINUTE_IS_DONE,
46 SECOND_IS_DONE
47 } whats_done_t;
48
49 #define SEC_PER_MIN 60
50 #define SEC_PER_HR (SEC_PER_MIN * 60)
51 #define SEC_PER_DAY (SEC_PER_HR * 24)
52 #define SEC_PER_WEEK (SEC_PER_DAY * 7)
53 #define SEC_PER_MONTH (SEC_PER_DAY * 30)
54 #define SEC_PER_YEAR (SEC_PER_DAY * 365)
55
56 #undef MAX_DURATION
57 #define MAX_DURATION TYPE_MAXIMUM(time_t)
58
59 /* Wrapper around strtoul that does not require a cast. */
60 static unsigned long
str_const_to_ul(cch_t * str,cch_t ** ppz,int base)61 str_const_to_ul (cch_t * str, cch_t ** ppz, int base)
62 {
63 char * pz;
64 int rv = strtoul (str, &pz, base);
65 *ppz = pz;
66 return rv;
67 }
68
69 /* Wrapper around strtol that does not require a cast. */
70 static long
str_const_to_l(cch_t * str,cch_t ** ppz,int base)71 str_const_to_l (cch_t * str, cch_t ** ppz, int base)
72 {
73 char * pz;
74 int rv = strtol (str, &pz, base);
75 *ppz = pz;
76 return rv;
77 }
78
79 /* Returns BASE + VAL * SCALE, interpreting BASE = BAD_TIME
80 with errno set as an error situation, and returning BAD_TIME
81 with errno set in an error situation. */
82 static time_t
scale_n_add(time_t base,time_t val,int scale)83 scale_n_add (time_t base, time_t val, int scale)
84 {
85 if (base == BAD_TIME)
86 {
87 if (errno == 0)
88 errno = EINVAL;
89 return BAD_TIME;
90 }
91
92 if (val > MAX_DURATION / scale)
93 {
94 errno = ERANGE;
95 return BAD_TIME;
96 }
97
98 val *= scale;
99 if (base > MAX_DURATION - val)
100 {
101 errno = ERANGE;
102 return BAD_TIME;
103 }
104
105 return base + val;
106 }
107
108 /* After a number HH has been parsed, parse subsequent :MM or :MM:SS. */
109 static time_t
parse_hr_min_sec(time_t start,cch_t * pz)110 parse_hr_min_sec (time_t start, cch_t * pz)
111 {
112 int lpct = 0;
113
114 errno = 0;
115
116 /* For as long as our scanner pointer points to a colon *AND*
117 we've not looped before, then keep looping. (two iterations max) */
118 while ((*pz == ':') && (lpct++ <= 1))
119 {
120 unsigned long v = str_const_to_ul (pz+1, &pz, 10);
121
122 if (errno != 0)
123 return BAD_TIME;
124
125 start = scale_n_add (v, start, 60);
126
127 if (errno != 0)
128 return BAD_TIME;
129 }
130
131 /* allow for trailing spaces */
132 while (isspace ((unsigned char)*pz))
133 pz++;
134 if (*pz != NUL)
135 {
136 errno = EINVAL;
137 return BAD_TIME;
138 }
139
140 return start;
141 }
142
143 /* Parses a value and returns BASE + value * SCALE, interpreting
144 BASE = BAD_TIME with errno set as an error situation, and returning
145 BAD_TIME with errno set in an error situation. */
146 static time_t
parse_scaled_value(time_t base,cch_t ** ppz,cch_t * endp,int scale)147 parse_scaled_value (time_t base, cch_t ** ppz, cch_t * endp, int scale)
148 {
149 cch_t * pz = *ppz;
150 time_t val;
151
152 if (base == BAD_TIME)
153 return base;
154
155 errno = 0;
156 val = str_const_to_ul (pz, &pz, 10);
157 if (errno != 0)
158 return BAD_TIME;
159 while (isspace ((unsigned char)*pz))
160 pz++;
161 if (pz != endp)
162 {
163 errno = EINVAL;
164 return BAD_TIME;
165 }
166
167 *ppz = pz;
168 return scale_n_add (base, val, scale);
169 }
170
171 /* Parses the syntax YEAR-MONTH-DAY.
172 PS points into the string, after "YEAR", before "-MONTH-DAY". */
173 static time_t
parse_year_month_day(cch_t * pz,cch_t * ps)174 parse_year_month_day (cch_t * pz, cch_t * ps)
175 {
176 time_t res = 0;
177
178 res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR);
179
180 pz++; /* over the first '-' */
181 ps = strchr (pz, '-');
182 if (ps == NULL)
183 {
184 errno = EINVAL;
185 return BAD_TIME;
186 }
187 res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH);
188
189 pz++; /* over the second '-' */
190 ps = pz + strlen (pz);
191 return parse_scaled_value (res, &pz, ps, SEC_PER_DAY);
192 }
193
194 /* Parses the syntax YYYYMMDD. */
195 static time_t
parse_yearmonthday(cch_t * in_pz)196 parse_yearmonthday (cch_t * in_pz)
197 {
198 time_t res = 0;
199 char buf[8];
200 cch_t * pz;
201
202 if (strlen (in_pz) != 8)
203 {
204 errno = EINVAL;
205 return BAD_TIME;
206 }
207
208 memcpy (buf, in_pz, 4);
209 buf[4] = NUL;
210 pz = buf;
211 res = parse_scaled_value (0, &pz, buf + 4, SEC_PER_YEAR);
212
213 memcpy (buf, in_pz + 4, 2);
214 buf[2] = NUL;
215 pz = buf;
216 res = parse_scaled_value (res, &pz, buf + 2, SEC_PER_MONTH);
217
218 memcpy (buf, in_pz + 6, 2);
219 buf[2] = NUL;
220 pz = buf;
221 return parse_scaled_value (res, &pz, buf + 2, SEC_PER_DAY);
222 }
223
224 /* Parses the syntax yy Y mm M ww W dd D. */
225 static time_t
parse_YMWD(cch_t * pz)226 parse_YMWD (cch_t * pz)
227 {
228 time_t res = 0;
229 cch_t * ps = strchr (pz, 'Y');
230 if (ps != NULL)
231 {
232 res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR);
233 pz++;
234 }
235
236 ps = strchr (pz, 'M');
237 if (ps != NULL)
238 {
239 res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH);
240 pz++;
241 }
242
243 ps = strchr (pz, 'W');
244 if (ps != NULL)
245 {
246 res = parse_scaled_value (res, &pz, ps, SEC_PER_WEEK);
247 pz++;
248 }
249
250 ps = strchr (pz, 'D');
251 if (ps != NULL)
252 {
253 res = parse_scaled_value (res, &pz, ps, SEC_PER_DAY);
254 pz++;
255 }
256
257 while (isspace ((unsigned char)*pz))
258 pz++;
259 if (*pz != NUL)
260 {
261 errno = EINVAL;
262 return BAD_TIME;
263 }
264
265 return res;
266 }
267
268 /* Parses the syntax HH:MM:SS.
269 PS points into the string, after "HH", before ":MM:SS". */
270 static time_t
parse_hour_minute_second(cch_t * pz,cch_t * ps)271 parse_hour_minute_second (cch_t * pz, cch_t * ps)
272 {
273 time_t res = 0;
274
275 res = parse_scaled_value (0, &pz, ps, SEC_PER_HR);
276
277 pz++;
278 ps = strchr (pz, ':');
279 if (ps == NULL)
280 {
281 errno = EINVAL;
282 return BAD_TIME;
283 }
284
285 res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN);
286
287 pz++;
288 ps = pz + strlen (pz);
289 return parse_scaled_value (res, &pz, ps, 1);
290 }
291
292 /* Parses the syntax HHMMSS. */
293 static time_t
parse_hourminutesecond(cch_t * in_pz)294 parse_hourminutesecond (cch_t * in_pz)
295 {
296 time_t res = 0;
297 char buf[4];
298 cch_t * pz;
299
300 if (strlen (in_pz) != 6)
301 {
302 errno = EINVAL;
303 return BAD_TIME;
304 }
305
306 memcpy (buf, in_pz, 2);
307 buf[2] = NUL;
308 pz = buf;
309 res = parse_scaled_value (0, &pz, buf + 2, SEC_PER_HR);
310
311 memcpy (buf, in_pz + 2, 2);
312 buf[2] = NUL;
313 pz = buf;
314 res = parse_scaled_value (res, &pz, buf + 2, SEC_PER_MIN);
315
316 memcpy (buf, in_pz + 4, 2);
317 buf[2] = NUL;
318 pz = buf;
319 return parse_scaled_value (res, &pz, buf + 2, 1);
320 }
321
322 /* Parses the syntax hh H mm M ss S. */
323 static time_t
parse_HMS(cch_t * pz)324 parse_HMS (cch_t * pz)
325 {
326 time_t res = 0;
327 cch_t * ps = strchr (pz, 'H');
328 if (ps != NULL)
329 {
330 res = parse_scaled_value (0, &pz, ps, SEC_PER_HR);
331 pz++;
332 }
333
334 ps = strchr (pz, 'M');
335 if (ps != NULL)
336 {
337 res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN);
338 pz++;
339 }
340
341 ps = strchr (pz, 'S');
342 if (ps != NULL)
343 {
344 res = parse_scaled_value (res, &pz, ps, 1);
345 pz++;
346 }
347
348 while (isspace ((unsigned char)*pz))
349 pz++;
350 if (*pz != NUL)
351 {
352 errno = EINVAL;
353 return BAD_TIME;
354 }
355
356 return res;
357 }
358
359 /* Parses a time (hours, minutes, seconds) specification in either syntax. */
360 static time_t
parse_time(cch_t * pz)361 parse_time (cch_t * pz)
362 {
363 cch_t * ps;
364 time_t res = 0;
365
366 /*
367 * Scan for a hyphen
368 */
369 ps = strchr (pz, ':');
370 if (ps != NULL)
371 {
372 res = parse_hour_minute_second (pz, ps);
373 }
374
375 /*
376 * Try for a 'H', 'M' or 'S' suffix
377 */
378 else if (ps = strpbrk (pz, "HMS"),
379 ps == NULL)
380 {
381 /* Its a YYYYMMDD format: */
382 res = parse_hourminutesecond (pz);
383 }
384
385 else
386 res = parse_HMS (pz);
387
388 return res;
389 }
390
391 /* Returns a substring of the given string, with spaces at the beginning and at
392 the end destructively removed, per SNOBOL. */
393 static char *
trim(char * pz)394 trim (char * pz)
395 {
396 /* trim leading white space */
397 while (isspace ((unsigned char)*pz))
398 pz++;
399
400 /* trim trailing white space */
401 {
402 char * pe = pz + strlen (pz);
403 while ((pe > pz) && isspace ((unsigned char)pe[-1]))
404 pe--;
405 *pe = NUL;
406 }
407
408 return pz;
409 }
410
411 /*
412 * Parse the year/months/days of a time period
413 */
414 static time_t
parse_period(cch_t * in_pz)415 parse_period (cch_t * in_pz)
416 {
417 char * pT;
418 char * ps;
419 char * pz = strdup (in_pz);
420 void * fptr = pz;
421 time_t res = 0;
422
423 if (pz == NULL)
424 {
425 errno = ENOMEM;
426 return BAD_TIME;
427 }
428
429 pT = strchr (pz, 'T');
430 if (pT != NULL)
431 {
432 *(pT++) = NUL;
433 pz = trim (pz);
434 pT = trim (pT);
435 }
436
437 /*
438 * Scan for a hyphen
439 */
440 ps = strchr (pz, '-');
441 if (ps != NULL)
442 {
443 res = parse_year_month_day (pz, ps);
444 }
445
446 /*
447 * Try for a 'Y', 'M' or 'D' suffix
448 */
449 else if (ps = strpbrk (pz, "YMWD"),
450 ps == NULL)
451 {
452 /* Its a YYYYMMDD format: */
453 res = parse_yearmonthday (pz);
454 }
455
456 else
457 res = parse_YMWD (pz);
458
459 if ((errno == 0) && (pT != NULL))
460 {
461 time_t val = parse_time (pT);
462 res = scale_n_add (res, val, 1);
463 }
464
465 free (fptr);
466 return res;
467 }
468
469 static time_t
parse_non_iso8601(cch_t * pz)470 parse_non_iso8601 (cch_t * pz)
471 {
472 whats_done_t whatd_we_do = NOTHING_IS_DONE;
473
474 time_t res = 0;
475
476 do {
477 time_t val;
478
479 errno = 0;
480 val = str_const_to_l (pz, &pz, 10);
481 if (errno != 0)
482 goto bad_time;
483
484 /* IF we find a colon, then we're going to have a seconds value.
485 We will not loop here any more. We cannot already have parsed
486 a minute value and if we've parsed an hour value, then the result
487 value has to be less than an hour. */
488 if (*pz == ':')
489 {
490 if (whatd_we_do >= MINUTE_IS_DONE)
491 break;
492
493 val = parse_hr_min_sec (val, pz);
494
495 if ((whatd_we_do == HOUR_IS_DONE) && (val >= SEC_PER_HR))
496 break;
497
498 return scale_n_add (res, val, 1);
499 }
500
501 {
502 unsigned int mult;
503
504 /* Skip over white space following the number we just parsed. */
505 while (isspace ((unsigned char)*pz))
506 pz++;
507
508 switch (*pz)
509 {
510 default: goto bad_time;
511 case NUL:
512 return scale_n_add (res, val, 1);
513
514 case 'y': case 'Y':
515 if (whatd_we_do >= YEAR_IS_DONE)
516 goto bad_time;
517 mult = SEC_PER_YEAR;
518 whatd_we_do = YEAR_IS_DONE;
519 break;
520
521 case 'M':
522 if (whatd_we_do >= MONTH_IS_DONE)
523 goto bad_time;
524 mult = SEC_PER_MONTH;
525 whatd_we_do = MONTH_IS_DONE;
526 break;
527
528 case 'W':
529 if (whatd_we_do >= WEEK_IS_DONE)
530 goto bad_time;
531 mult = SEC_PER_WEEK;
532 whatd_we_do = WEEK_IS_DONE;
533 break;
534
535 case 'd': case 'D':
536 if (whatd_we_do >= DAY_IS_DONE)
537 goto bad_time;
538 mult = SEC_PER_DAY;
539 whatd_we_do = DAY_IS_DONE;
540 break;
541
542 case 'h':
543 if (whatd_we_do >= HOUR_IS_DONE)
544 goto bad_time;
545 mult = SEC_PER_HR;
546 whatd_we_do = HOUR_IS_DONE;
547 break;
548
549 case 'm':
550 if (whatd_we_do >= MINUTE_IS_DONE)
551 goto bad_time;
552 mult = SEC_PER_MIN;
553 whatd_we_do = MINUTE_IS_DONE;
554 break;
555
556 case 's':
557 mult = 1;
558 whatd_we_do = SECOND_IS_DONE;
559 break;
560 }
561
562 res = scale_n_add (res, val, mult);
563
564 pz++;
565 while (isspace ((unsigned char)*pz))
566 pz++;
567 if (*pz == NUL)
568 return res;
569
570 if (! isdigit ((unsigned char)*pz))
571 break;
572 }
573
574 } while (whatd_we_do < SECOND_IS_DONE);
575
576 bad_time:
577 errno = EINVAL;
578 return BAD_TIME;
579 }
580
581 time_t
parse_duration(char const * pz)582 parse_duration (char const * pz)
583 {
584 while (isspace ((unsigned char)*pz))
585 pz++;
586
587 switch (*pz)
588 {
589 case 'P':
590 return parse_period (pz + 1);
591
592 case 'T':
593 return parse_time (pz + 1);
594
595 default:
596 if (isdigit ((unsigned char)*pz))
597 return parse_non_iso8601 (pz);
598
599 errno = EINVAL;
600 return BAD_TIME;
601 }
602 }
603
604 /*
605 * Local Variables:
606 * mode: C
607 * c-file-style: "gnu"
608 * indent-tabs-mode: nil
609 * End:
610 * end of parse-duration.c */
611