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