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