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