| /* Parse a time duration and return a seconds count | |
| Copyright (C) 2008-2026 Free Software Foundation, Inc. | |
| Written by Bruce Korb <bkorb@gnu.org>, 2008. | |
| This file 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 file 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 allow a sign. */ | |
| static unsigned long | |
| str_const_to_ul (cch_t * str, cch_t ** ppz, int base) | |
| { | |
| cch_t * orig_str = str; | |
| while (isspace ((unsigned char) *str)) | |
| str++; | |
| if (isdigit ((unsigned char) *str)) | |
| { | |
| unsigned long ret = strtoul (str, (char **)ppz, base); | |
| if (*ppz == str) | |
| *ppz = orig_str; | |
| return ret; | |
| } | |
| else | |
| { | |
| *ppz = orig_str; | |
| return 0; | |
| } | |
| } | |
| /* 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, (char **)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; | |
| if (base == BAD_TIME) | |
| return base; | |
| errno = 0; | |
| time_t 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 = 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 = strnul (pz); | |
| return parse_scaled_value (res, &pz, ps, SEC_PER_DAY); | |
| } | |
| /* Parses the syntax YYYYMMDD. */ | |
| static time_t | |
| parse_yearmonthday (cch_t * in_pz) | |
| { | |
| if (strlen (in_pz) != 8) | |
| { | |
| errno = EINVAL; | |
| return BAD_TIME; | |
| } | |
| char buf[8]; | |
| memcpy (buf, in_pz, 4); | |
| buf[4] = NUL; | |
| cch_t *pz = buf; | |
| time_t 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++; | |
| } | |
| } | |
| { | |
| cch_t *ps = strchr (pz, 'M'); | |
| if (ps != NULL) | |
| { | |
| res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH); | |
| pz++; | |
| } | |
| } | |
| { | |
| cch_t *ps = strchr (pz, 'W'); | |
| if (ps != NULL) | |
| { | |
| res = parse_scaled_value (res, &pz, ps, SEC_PER_WEEK); | |
| pz++; | |
| } | |
| } | |
| { | |
| cch_t *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 = 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 = strnul (pz); | |
| return parse_scaled_value (res, &pz, ps, 1); | |
| } | |
| /* Parses the syntax HHMMSS. */ | |
| static time_t | |
| parse_hourminutesecond (cch_t * in_pz) | |
| { | |
| if (strlen (in_pz) != 6) | |
| { | |
| errno = EINVAL; | |
| return BAD_TIME; | |
| } | |
| char buf[4]; | |
| memcpy (buf, in_pz, 2); | |
| buf[2] = NUL; | |
| cch_t *pz = buf; | |
| time_t 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++; | |
| } | |
| } | |
| { | |
| cch_t *ps = strchr (pz, 'M'); | |
| if (ps != NULL) | |
| { | |
| res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN); | |
| pz++; | |
| } | |
| } | |
| { | |
| cch_t *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) | |
| { | |
| time_t res; | |
| /* | |
| * Scan for a hyphen | |
| */ | |
| cch_t *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 = strnul (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 *pz = strdup (in_pz); | |
| if (pz == NULL) | |
| { | |
| errno = ENOMEM; | |
| return BAD_TIME; | |
| } | |
| void * fptr = pz; | |
| char *pT = strchr (pz, 'T'); | |
| if (pT != NULL) | |
| { | |
| *(pT++) = NUL; | |
| pz = trim (pz); | |
| pT = trim (pT); | |
| } | |
| time_t res; | |
| /* | |
| * Scan for a hyphen | |
| */ | |
| char *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 { | |
| errno = 0; | |
| time_t 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 */ |