/* Parse a time duration and return a seconds count Copyright (C) 2008, 2009, 2010 Free Software Foundation, Inc. Written by Bruce Korb , 2008. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include /* Specification. */ #include "parse-duration.h" #include #include #include #include #include #include #include "xalloc.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) #define TIME_MAX 0x7FFFFFFF /* Wrapper around strtoul that does not require a cast. */ static unsigned long inline str_const_to_ul (cch_t * str, cch_t ** ppz, int base) { return strtoul (str, (char **)ppz, base); } /* Wrapper around strtol that does not require a cast. */ static long inline 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 inline 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 > TIME_MAX / scale) { errno = ERANGE; return BAD_TIME; } val *= scale; if (base > TIME_MAX - 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. */ 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 * pz = xstrdup (in_pz); char * pT = strchr (pz, 'T'); char * ps; void * fptr = pz; time_t res = 0; if (pT != NUL) { *(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 */