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