/*      $NetBSD: parse-duration.c,v 1.11 2024/08/18 20:47:25 christos Exp $     */

/* Parse a time duration and return a seconds count
  Copyright (C) 2008-2018 Free Software Foundation, Inc.
  Written by Bruce Korb <[email protected]>, 2008.

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation; either version 2.1 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with this program.  If not, see <https://www.gnu.org/licenses/>.  */

#include <config.h>

/* Specification.  */
#include "parse-duration.h"

#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "intprops.h"

#ifndef NUL
#define NUL '\0'
#endif

#define cch_t char const

typedef enum {
 NOTHING_IS_DONE,
 YEAR_IS_DONE,
 MONTH_IS_DONE,
 WEEK_IS_DONE,
 DAY_IS_DONE,
 HOUR_IS_DONE,
 MINUTE_IS_DONE,
 SECOND_IS_DONE
} whats_done_t;

#define SEC_PER_MIN     60
#define SEC_PER_HR      (SEC_PER_MIN * 60)
#define SEC_PER_DAY     (SEC_PER_HR  * 24)
#define SEC_PER_WEEK    (SEC_PER_DAY * 7)
#define SEC_PER_MONTH   (SEC_PER_DAY * 30)
#define SEC_PER_YEAR    (SEC_PER_DAY * 365)

#undef  MAX_DURATION
#define MAX_DURATION    TYPE_MAXIMUM(time_t)

/* Wrapper around strtoul that does not require a cast.  */
static unsigned long
str_const_to_ul (cch_t * str, cch_t ** ppz, int base)
{
 return strtoul (str, __UNCONST(ppz), base);
}

/* Wrapper around strtol that does not require a cast.  */
static long
str_const_to_l (cch_t * str, cch_t ** ppz, int base)
{
 return strtol (str, __UNCONST(ppz), base);
}

/* Returns BASE + VAL * SCALE, interpreting BASE = BAD_TIME
  with errno set as an error situation, and returning BAD_TIME
  with errno set in an error situation.  */
static time_t
scale_n_add (time_t base, time_t val, int scale)
{
 if (base == BAD_TIME)
   {
     if (errno == 0)
       errno = EINVAL;
     return BAD_TIME;
   }

 if (val > MAX_DURATION / scale)
   {
     errno = ERANGE;
     return BAD_TIME;
   }

 val *= scale;
 if (base > MAX_DURATION - val)
   {
     errno = ERANGE;
     return BAD_TIME;
   }

 return base + val;
}

/* After a number HH has been parsed, parse subsequent :MM or :MM:SS.  */
static time_t
parse_hr_min_sec (time_t start, cch_t * pz)
{
 int lpct = 0;

 errno = 0;

 /* For as long as our scanner pointer points to a colon *AND*
    we've not looped before, then keep looping.  (two iterations max) */
 while ((*pz == ':') && (lpct++ <= 1))
   {
     unsigned long v = str_const_to_ul (pz+1, &pz, 10);

     if (errno != 0)
       return BAD_TIME;

     start = scale_n_add (v, start, 60);

     if (errno != 0)
       return BAD_TIME;
   }

 /* allow for trailing spaces */
 while (isspace ((unsigned char)*pz))
   pz++;
 if (*pz != NUL)
   {
     errno = EINVAL;
     return BAD_TIME;
   }

 return start;
}

/* Parses a value and returns BASE + value * SCALE, interpreting
  BASE = BAD_TIME with errno set as an error situation, and returning
  BAD_TIME with errno set in an error situation.  */
static time_t
parse_scaled_value (time_t base, cch_t ** ppz, cch_t * endp, int scale)
{
 cch_t * pz = *ppz;
 time_t val;

 if (base == BAD_TIME)
   return base;

 errno = 0;
 val = str_const_to_ul (pz, &pz, 10);
 if (errno != 0)
   return BAD_TIME;
 while (isspace ((unsigned char)*pz))
   pz++;
 if (pz != endp)
   {
     errno = EINVAL;
     return BAD_TIME;
   }

 *ppz = pz;
 return scale_n_add (base, val, scale);
}

/* Parses the syntax YEAR-MONTH-DAY.
  PS points into the string, after "YEAR", before "-MONTH-DAY".  */
static time_t
parse_year_month_day (cch_t * pz, cch_t * ps)
{
 time_t res = 0;

 res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR);

 pz++; /* over the first '-' */
 ps = strchr (pz, '-');
 if (ps == NULL)
   {
     errno = EINVAL;
     return BAD_TIME;
   }
 res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH);

 pz++; /* over the second '-' */
 ps = pz + strlen (pz);
 return parse_scaled_value (res, &pz, ps, SEC_PER_DAY);
}

/* Parses the syntax YYYYMMDD.  */
static time_t
parse_yearmonthday (cch_t * in_pz)
{
 time_t res = 0;
 char   buf[8];
 cch_t * pz;

 if (strlen (in_pz) != 8)
   {
     errno = EINVAL;
     return BAD_TIME;
   }

 memcpy (buf, in_pz, 4);
 buf[4] = NUL;
 pz = buf;
 res = parse_scaled_value (0, &pz, buf + 4, SEC_PER_YEAR);

 memcpy (buf, in_pz + 4, 2);
 buf[2] = NUL;
 pz =   buf;
 res = parse_scaled_value (res, &pz, buf + 2, SEC_PER_MONTH);

 memcpy (buf, in_pz + 6, 2);
 buf[2] = NUL;
 pz =   buf;
 return parse_scaled_value (res, &pz, buf + 2, SEC_PER_DAY);
}

/* Parses the syntax yy Y mm M ww W dd D.  */
static time_t
parse_YMWD (cch_t * pz)
{
 time_t res = 0;
 cch_t * ps = strchr (pz, 'Y');
 if (ps != NULL)
   {
     res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR);
     pz++;
   }

 ps = strchr (pz, 'M');
 if (ps != NULL)
   {
     res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH);
     pz++;
   }

 ps = strchr (pz, 'W');
 if (ps != NULL)
   {
     res = parse_scaled_value (res, &pz, ps, SEC_PER_WEEK);
     pz++;
   }

 ps = strchr (pz, 'D');
 if (ps != NULL)
   {
     res = parse_scaled_value (res, &pz, ps, SEC_PER_DAY);
     pz++;
   }

 while (isspace ((unsigned char)*pz))
   pz++;
 if (*pz != NUL)
   {
     errno = EINVAL;
     return BAD_TIME;
   }

 return res;
}

/* Parses the syntax HH:MM:SS.
  PS points into the string, after "HH", before ":MM:SS".  */
static time_t
parse_hour_minute_second (cch_t * pz, cch_t * ps)
{
 time_t res = 0;

 res = parse_scaled_value (0, &pz, ps, SEC_PER_HR);

 pz++;
 ps = strchr (pz, ':');
 if (ps == NULL)
   {
     errno = EINVAL;
     return BAD_TIME;
   }

 res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN);

 pz++;
 ps = pz + strlen (pz);
 return parse_scaled_value (res, &pz, ps, 1);
}

/* Parses the syntax HHMMSS.  */
static time_t
parse_hourminutesecond (cch_t * in_pz)
{
 time_t res = 0;
 char   buf[4];
 cch_t * pz;

 if (strlen (in_pz) != 6)
   {
     errno = EINVAL;
     return BAD_TIME;
   }

 memcpy (buf, in_pz, 2);
 buf[2] = NUL;
 pz = buf;
 res = parse_scaled_value (0, &pz, buf + 2, SEC_PER_HR);

 memcpy (buf, in_pz + 2, 2);
 buf[2] = NUL;
 pz =   buf;
 res = parse_scaled_value (res, &pz, buf + 2, SEC_PER_MIN);

 memcpy (buf, in_pz + 4, 2);
 buf[2] = NUL;
 pz =   buf;
 return parse_scaled_value (res, &pz, buf + 2, 1);
}

/* Parses the syntax hh H mm M ss S.  */
static time_t
parse_HMS (cch_t * pz)
{
 time_t res = 0;
 cch_t * ps = strchr (pz, 'H');
 if (ps != NULL)
   {
     res = parse_scaled_value (0, &pz, ps, SEC_PER_HR);
     pz++;
   }

 ps = strchr (pz, 'M');
 if (ps != NULL)
   {
     res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN);
     pz++;
   }

 ps = strchr (pz, 'S');
 if (ps != NULL)
   {
     res = parse_scaled_value (res, &pz, ps, 1);
     pz++;
   }

 while (isspace ((unsigned char)*pz))
   pz++;
 if (*pz != NUL)
   {
     errno = EINVAL;
     return BAD_TIME;
   }

 return res;
}

/* Parses a time (hours, minutes, seconds) specification in either syntax.  */
static time_t
parse_time (cch_t * pz)
{
 cch_t * ps;
 time_t  res = 0;

 /*
  *  Scan for a hyphen
  */
 ps = strchr (pz, ':');
 if (ps != NULL)
   {
     res = parse_hour_minute_second (pz, ps);
   }

 /*
  *  Try for a 'H', 'M' or 'S' suffix
  */
 else if (ps = strpbrk (pz, "HMS"),
          ps == NULL)
   {
     /* Its a YYYYMMDD format: */
     res = parse_hourminutesecond (pz);
   }

 else
   res = parse_HMS (pz);

 return res;
}

/* Returns a substring of the given string, with spaces at the beginning and at
  the end destructively removed, per SNOBOL.  */
static char *
trim (char * pz)
{
 /* trim leading white space */
 while (isspace ((unsigned char)*pz))
   pz++;

 /* trim trailing white space */
 {
   char * pe = pz + strlen (pz);
   while ((pe > pz) && isspace ((unsigned char)pe[-1]))
     pe--;
   *pe = NUL;
 }

 return pz;
}

/*
*  Parse the year/months/days of a time period
*/
static time_t
parse_period (cch_t * in_pz)
{
 char * pT;
 char * ps;
 char * pz   = strdup (in_pz);
 void * fptr = pz;
 time_t res  = 0;

 if (pz == NULL)
   {
     errno = ENOMEM;
     return BAD_TIME;
   }

 pT = strchr (pz, 'T');
 if (pT != NULL)
   {
     *(pT++) = NUL;
     pz = trim (pz);
     pT = trim (pT);
   }

 /*
  *  Scan for a hyphen
  */
 ps = strchr (pz, '-');
 if (ps != NULL)
   {
     res = parse_year_month_day (pz, ps);
   }

 /*
  *  Try for a 'Y', 'M' or 'D' suffix
  */
 else if (ps = strpbrk (pz, "YMWD"),
          ps == NULL)
   {
     /* Its a YYYYMMDD format: */
     res = parse_yearmonthday (pz);
   }

 else
   res = parse_YMWD (pz);

 if ((errno == 0) && (pT != NULL))
   {
     time_t val = parse_time (pT);
     res = scale_n_add (res, val, 1);
   }

 free (fptr);
 return res;
}

static time_t
parse_non_iso8601 (cch_t * pz)
{
 whats_done_t whatd_we_do = NOTHING_IS_DONE;

 time_t res = 0;

 do  {
   time_t val;

   errno = 0;
   val = str_const_to_l (pz, &pz, 10);
   if (errno != 0)
     goto bad_time;

   /*  IF we find a colon, then we're going to have a seconds value.
       We will not loop here any more.  We cannot already have parsed
       a minute value and if we've parsed an hour value, then the result
       value has to be less than an hour. */
   if (*pz == ':')
     {
       if (whatd_we_do >= MINUTE_IS_DONE)
         break;

       val = parse_hr_min_sec (val, pz);

       if ((whatd_we_do == HOUR_IS_DONE) && (val >= SEC_PER_HR))
         break;

       return scale_n_add (res, val, 1);
     }

   {
     unsigned int mult;

     /*  Skip over white space following the number we just parsed. */
     while (isspace ((unsigned char)*pz))
       pz++;

     switch (*pz)
       {
       default:  goto bad_time;
       case NUL:
         return scale_n_add (res, val, 1);

       case 'y': case 'Y':
         if (whatd_we_do >= YEAR_IS_DONE)
           goto bad_time;
         mult = SEC_PER_YEAR;
         whatd_we_do = YEAR_IS_DONE;
         break;

       case 'M':
         if (whatd_we_do >= MONTH_IS_DONE)
           goto bad_time;
         mult = SEC_PER_MONTH;
         whatd_we_do = MONTH_IS_DONE;
         break;

       case 'W':
         if (whatd_we_do >= WEEK_IS_DONE)
           goto bad_time;
         mult = SEC_PER_WEEK;
         whatd_we_do = WEEK_IS_DONE;
         break;

       case 'd': case 'D':
         if (whatd_we_do >= DAY_IS_DONE)
           goto bad_time;
         mult = SEC_PER_DAY;
         whatd_we_do = DAY_IS_DONE;
         break;

       case 'h':
         if (whatd_we_do >= HOUR_IS_DONE)
           goto bad_time;
         mult = SEC_PER_HR;
         whatd_we_do = HOUR_IS_DONE;
         break;

       case 'm':
         if (whatd_we_do >= MINUTE_IS_DONE)
           goto bad_time;
         mult = SEC_PER_MIN;
         whatd_we_do = MINUTE_IS_DONE;
         break;

       case 's':
         mult = 1;
         whatd_we_do = SECOND_IS_DONE;
         break;
       }

     res = scale_n_add (res, val, mult);

     pz++;
     while (isspace ((unsigned char)*pz))
       pz++;
     if (*pz == NUL)
       return res;

     if (! isdigit ((unsigned char)*pz))
       break;
   }

 } while (whatd_we_do < SECOND_IS_DONE);

bad_time:
 errno = EINVAL;
 return BAD_TIME;
}

time_t
parse_duration (char const * pz)
{
 while (isspace ((unsigned char)*pz))
   pz++;

 switch (*pz)
   {
   case 'P':
     return parse_period (pz + 1);

   case 'T':
     return parse_time (pz + 1);

   default:
     if (isdigit ((unsigned char)*pz))
       return parse_non_iso8601 (pz);

     errno = EINVAL;
     return BAD_TIME;
   }
}

/*
* Local Variables:
* mode: C
* c-file-style: "gnu"
* indent-tabs-mode: nil
* End:
* end of parse-duration.c */