xref: /netbsd-src/external/bsd/ntp/dist/sntp/libopts/parse-duration.c (revision 9ddb6ab554e70fb9bbd90c3d96b812bc57755a14)
1 /*	$NetBSD: parse-duration.c,v 1.3 2012/02/03 21:36:40 christos Exp $	*/
2 
3 /* Parse a time duration and return a seconds count
4    Copyright (C) 2008-2011 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 General Public License as published by
9    the Free Software Foundation; either version 3 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 General Public License for more details.
16 
17    You should have received a copy of the GNU 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 inline 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 inline 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 inline 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