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