1/* Parse a time duration and return a seconds count
2   Copyright (C) 2008-2015 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 <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
38typedef 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.  */
60static unsigned long
61str_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.  */
70static long
71str_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.  */
82static time_t
83scale_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.  */
109static time_t
110parse_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.  */
146static time_t
147parse_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".  */
173static time_t
174parse_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.  */
195static time_t
196parse_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.  */
225static time_t
226parse_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".  */
270static time_t
271parse_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.  */
293static time_t
294parse_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.  */
323static time_t
324parse_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.  */
360static time_t
361parse_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.  */
393static char *
394trim (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 */
414static time_t
415parse_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
469static time_t
470parse_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
581time_t
582parse_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