xref: /freebsd/contrib/ntp/sntp/libopts/parse-duration.c (revision 59c8e88e72633afbc47a4ace0d2170d00d51f7dc)
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
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
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
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
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
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
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
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
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
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
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
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
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 *
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
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
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
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