/** * Copyright © 2015, 2016 Mattias Andrée * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #define _DEFAULT_SOURCE #include "parse_time.h" #include "common.h" #include #include #include #include #include #include #define W(...) do { __VA_ARGS__ } while (0) /** * The number of seconds in a day. */ #define ONE_DAY (time_t)(24 * 60 * 60) /** * Set errno and return. * * @return e:int The error number. */ #define FAIL(e) return errno = (e), -1 /** * Set errno to EINVAL and return if a condition is not met. * * @return cond:int The condition. */ #define REQUIRE(cond) W(if (!(cond)) FAIL(EINVAL);) /** * `a *= b` with overflow check. */ #define MUL(a, b) W(if (a > timemax / (b)) FAIL(ERANGE); else a *= (b);) /** * `a += b` with overflow check. */ #define ADD(a, b) W(if (a > timemax - (b)) FAIL(ERANGE); else a += (b);) /** * The name of the process. */ extern char *argv0; /** * The highest value that can be stored in `time_t`. */ static const time_t timemax = (sizeof(time_t) == sizeof(long long int)) ? (time_t)LLONG_MAX : (time_t)LONG_MAX; /** * Wrapper for `strtoll` and `strtol` that selects * the appropriate function for `time_t`, does not * allow whitespace or signs. It also forces the * base to be 10, and will only return 0 on error, * and will set `errno` on success. * * @param str Please see strtol(3), first parameter. * @param end Please see strtol(3), second parameter. * @return Please see strtol(3), only 0 may be * returned at error. * * @throws 0 On success. * @throws Please see strtol(3). */ static time_t strtotime(const char *str, const char **end) { #ifdef __GNUC__ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wcast-qual" #endif time_t rc; REQUIRE(isdigit(*str)); errno = 0; /* The outer if-statement is for when `time_t` is update to `long long int`. * which is need to avoid the year 2038 problem. Be we have to use `strtol` * if `time_t` is `long int`, otherwise we will not detect overflow. */ if (sizeof(time_t) == sizeof(long long int)) rc = (time_t)strtoll(str, (char **)end, 10); else rc = (time_t)strtol(str, (char **)end, 10); return errno ? 0 : rc; #ifdef __GNUC__ # pragma GCC diagnostic pop #endif } /** * Parse a time on the format HH:MM[:SS]. * * @param str Pointer to time string, will be updated to the end * of the parsing of it. * @param ts Output parameter for the POSIX time the string * represents. * @return 0 on success, -1 on error. * * @throws EINVAL `str` could not be parsed. * @throws ERANGE `str` specifies a time beyond what can be stored. */ static int parse_time_time(const char **str, struct timespec *ts) { time_t tm; /* Hours. */ t (ts->tv_sec = strtotime(*str, str), errno); /* Must not be restricted to 23, beyond 24 is legal. */ MUL(ts->tv_sec, (time_t)(60 * 60)); /* Minutes. */ REQUIRE(*(*str)++ == ':'); t (tm = strtotime(*str, str), errno); REQUIRE(tm < 60); MUL(tm, (time_t)60); ADD(ts->tv_sec, tm); /* Seconds. */ if (*(*str)++ != ':') return 0; t (tm = strtotime(*str, str), errno); /* Do not restrict to 59, or 60, there can be more than +1 leap second. */ ADD(ts->tv_sec, tm); return 0; fail: return -1; } /** * Parse a time with only a second-count. * * @param str Pointer to time string, will be updated to the end * of the parsing of it. * @param ts Output parameter for the POSIX time the string * represents. * @return 0 on success, -1 on error. * * @throws EINVAL `str` could not be parsed. * @throws ERANGE `str` specifies a time beyond what can be stored. */ static int parse_time_seconds(const char **str, struct timespec *ts) { ts->tv_sec = strtotime(*str, str); return errno ? -1 : 0; } /** * Trivial time-parsing. * * @param str The time string. * @param ts Output parameter for the POSIX time the string * represents. If the time is in the pasted, but * within a day, one day is added. This is in case * you are using a external parser that did not * release they you wanted a future time but * thought you wanted the time to be the closed or * in the same day. * @param clk Output parameter for the clock in which the * returned time is specified. * @return 0 on success, -1 on error. * * @throws EINVAL `str` could not be parsed. * @throws ERANGE `str` specifies a time beyond what can be stored. * @throws EDOM The specified the was over a day ago. * * @futuredirection Add environment variable that allows * parsing through another program. */ int parse_time(const char *str, struct timespec *ts, clockid_t *clk) { #define FIX_NSEC(T) (((T)->tv_nsec >= 1000000000L) ? ((T)->tv_sec += 1, (T)->tv_nsec -= 1000000000L) : 0L) struct timespec now; int points, plus = *str == '+'; const char *start = str; time_t adj; ts->tv_nsec = 0; /* Get current time and clock. */ *clk = plus ? CLOCK_BOOTTIME : CLOCK_REALTIME; clock_gettime(*clk, &now); /* Mañana? */ if (!strcmp(str, "mañana")) { /* Do not documented. */ ts->tv_sec = now.tv_sec + ONE_DAY; ts->tv_nsec = now.tv_nsec; return 0; } /* HH:MM[:SS[.NNNNNNNNN]] or seconds? */ if (strchr(str += plus, ':')) { t (parse_time_time(&str, ts)); adj = now.tv_sec - (now.tv_sec % ONE_DAY); ADD(ts->tv_sec, adj); /* In case the HH is really large. */ } else { t (parse_time_seconds(&str, ts)); } /* Parse up to nanosecond resolution. */ if (*str != '.') goto no_nanoseconds; for (points = 0, str++; isdigit(*str); points++, str++) { if (points < 9) { ts->tv_nsec *= 10; ts->tv_nsec += *str & 15; } else if ((points == 9) && (*str >= '5')) { ts->tv_nsec += 1; /* I would like to have FIX_NSEC here, but the * compile will complain and it is not worth it. */ } } while (points++ < 9) ts->tv_nsec *= 10; FIX_NSEC(ts); no_nanoseconds: /* Check for error at end, and missing explicit UTC. */ if (*str) { REQUIRE(*clk != CLOCK_BOOTTIME); while (*str == ' ') str++; REQUIRE(!strcasecmp(str, "Z") || !strcasecmp(str, "UTC")); } else if (*clk == CLOCK_REALTIME) { fprintf(stderr, "%s: warning: parsing as UTC, you can avoid this warning " "by adding a 'Z' at the end of the time argument.\n", argv0); } /* Add offset or djust the day? */ if (*clk == CLOCK_BOOTTIME) { ts->tv_sec += now.tv_sec; ts->tv_nsec += now.tv_nsec; FIX_NSEC(ts); } else if (ts->tv_sec < now.tv_sec) { /* Ignore partial second. */ ts->tv_sec += ONE_DAY; if (ts->tv_sec < now.tv_sec) FAIL(EDOM); if (!strchr(start, ':')) fprintf(stderr, "%s: warning: the specified time is in the past, " "it is being adjust to be tomorrow instead.\n", argv0); } return 0; fail: return -1; }