handle strftime() more defensively
[libi2ncommon] / src / timefunc.cpp
index ab5f6dc..88e6c33 100644 (file)
@@ -24,6 +24,8 @@ on this file might be covered by the GNU General Public License.
  *
  */
 
+#include <cstdio>
+#include <errno.h>
 #include <string>
 #include <sstream>
 #include <iostream>
@@ -37,7 +39,6 @@ on this file might be covered by the GNU General Public License.
 #include <unistd.h>
 #include <string.h>
 #include <sys/timeb.h>
-#include <sys/syscall.h>
 
 #include <timefunc.hxx>
 #include <i18n.h>
@@ -69,9 +70,10 @@ double prec_time(void)
 }
 
 // converts ISO-DATE: 2003-06-13
-int date_to_seconds(const std::string &date)
+time_t date_to_seconds(const std::string &date)
 {
-    int rtn = -1, year = -1, month = -1, day = -1;
+    time_t rtn = 0;
+    int year = -1, month = -1, day = -1;
     
     string::size_type pos = date.find("-");
     if (pos == string::npos)
@@ -118,10 +120,8 @@ string make_nice_time(int seconds)
     int hours,minutes;
     split_daysec(seconds,&hours,&minutes,&seconds);
     
-    if (days==1)
-        out << i18n("1 day") << ", ";
-    else if (days>1)
-        out << days << ' ' << i18n("days") << ", ";
+    if (days>0)
+        out << days << " " << i18n_plural("day", "days", days) << ", ";
 
     out << setfill('0');
     out << setw(2) << hours << ':' << setw(2) << minutes << ':' << setw(2) << seconds;
@@ -129,7 +129,7 @@ string make_nice_time(int seconds)
     return out.str();
 }
 
-string format_full_time(int seconds)
+string format_full_time(time_t seconds)
 {
     char buf[50];
     memset (buf, 0, 50);
@@ -141,6 +141,18 @@ string format_full_time(int seconds)
     return string(buf);
 }
 
+string format_date(time_t seconds)
+{
+    char buf[50];
+    memset (buf, 0, 50);
+    struct tm ta;
+    if (localtime_r((time_t *)&seconds, &ta) == NULL)
+        memset (&ta, 0, sizeof(struct tm));
+
+    strftime (buf, 49, "%d.%m.%Y", &ta);
+    return string(buf);
+}
+
 void seconds_to_hour_minute(int seconds, int *hour, int *minute)
 {
     if (hour != NULL) {
@@ -712,7 +724,7 @@ Intervals& Intervals::operator-=(const Intervals& other)
 bool monotonic_clock_gettime(long int& seconds, long int& nano_seconds)
 {
     struct timespec tp[1];
-    int res= ::syscall(__NR_clock_gettime, CLOCK_MONOTONIC, tp);
+    int res= clock_gettime (CLOCK_MONOTONIC, tp);
     if (0 == res)
     {
         seconds= tp->tv_sec;
@@ -751,7 +763,7 @@ long long monotonic_clock_gettime_nano()
 bool realtime_clock_gettime(long int& seconds, long int& nano_seconds)
 {
     struct timespec tp[1];
-    int res= ::syscall(__NR_clock_gettime, CLOCK_REALTIME, tp);
+    int res= clock_gettime(CLOCK_REALTIME, tp);
     if (0 == res)
     {
         seconds= tp->tv_sec;
@@ -761,3 +773,551 @@ bool realtime_clock_gettime(long int& seconds, long int& nano_seconds)
 } // eo realtime_clock_gettime(long int&,long int&)
 
 
+/*
+ * There is a discrepancy of one input character
+ * due to the lack of sign handling in strptime(3):
+ *
+ *      - strftime(3) needs the year specified as %5Y to account for the
+ *        leading dash;
+ *      - strptime(3) will not parse the leading dash with that format
+ *        but apart from that it works well.
+ */
+
+namespace iso8601 {
+
+    /*
+     * max: -YYYYYYYYYY-MM-DDThh:mm:ssZ+zzzz ⇒ 32
+     * That is assuming the year in broken down time is an int we
+     * need to reserve ten decimal places.
+     */
+//  static_assert (sizeof (((struct tm *)NULL)->tm_year) == 4);
+    static const size_t bufsize = 33;
+
+    enum kind {
+        d    = 0,
+        t    = 1,
+        tz   = 2,
+        dt   = 3,
+        dtz  = 4,
+        ISO8601_SIZE = 5,
+    };
+
+    /*
+     * Unfortunately the glibc strptime(3) on the Intranator trips over
+     * the length specifier in field descriptors so we can’t reuse the
+     * formatters here. This problem is fixed in newer glibc. For the time
+     * being we keep two tables of formatters and choose the appropriate
+     * at runtime.
+     */
+
+    static const char *const formatter [ISO8601_SIZE] =
+    { /* [iso8601::d  ] = */ "%4Y-%m-%d",
+      /* [iso8601::t  ] = */ "%T",
+      /* [iso8601::tz ] = */ "%TZ%z",
+      /* [iso8601::dt ] = */ "%4Y-%m-%dT%T",
+      /* [iso8601::dtz] = */ "%4Y-%m-%dT%TZ%z",
+    };
+
+    static const char *const scanner [ISO8601_SIZE] =
+    { /* [iso8601::d  ] = */ "%Y-%m-%d",
+      /* [iso8601::t  ] = */ "%T",
+      /* [iso8601::tz ] = */ "%TZ%z",
+      /* [iso8601::dt ] = */ "%Y-%m-%dT%T",
+      /* [iso8601::dtz] = */ "%Y-%m-%dT%TZ%z",
+    };
+
+    static inline const char *
+    pick_fmt (const bool date, const bool time, const bool tz, const bool scan=false)
+    {
+        const char *const  *table  = scan ? iso8601::scanner : iso8601::formatter;
+        enum iso8601::kind  format = iso8601::dtz;
+
+        if (date) {
+            if (time) {
+                if (tz) {
+                    format = iso8601::dtz;
+                } else {
+                    format = iso8601::dt;
+                }
+            } else {
+                format = iso8601::d;
+            }
+        } else if (time && tz) {
+            format = iso8601::tz;
+        } else {
+            format = iso8601::t; /* default to %T */
+        }
+
+        return table [format];
+    }
+
+} /* [iso8601] */
+
+
+namespace {
+
+    static inline int flip_tm_year (const int y)
+    { return (y + 1900) * -1 - 1900; }
+} /* [namespace] */
+
+/**
+ * @brief         Format a time structure according to ISO-8601, e. g.
+ *                “2018-01-09T10:40:00Z+0100”; see \c strftime(3) for
+ *                the details.
+ *
+ * @param tm      Time to format as broken-down \c struct tm.
+ * @param date    Include the day part ([-]YYYY-MM-DD).
+ * @param time    Include the time part (hh:mm:ss).
+ * @param tz      Include the timezone ([±]ZZZZ); only needed if
+ *                \c time is requested as well.
+ *
+ * @return        The formatted timestamp.
+ */
+std::string format_iso8601 (const struct tm &tm, const bool date,
+                            const bool time, const bool tz)
+{
+    struct tm tmp;
+    char buf [iso8601::bufsize] = { 0 };
+    char *start = &buf [0];
+    const char *format = iso8601::pick_fmt (date, time, tz);
+
+    memcpy (&tmp, &tm, sizeof (tmp));
+
+    if (tmp.tm_year < -1900) { /* negative year */
+        *start = '-';
+        start++;
+        tmp.tm_year = flip_tm_year (tmp.tm_year);
+    }
+
+    /*
+     * The sign is *always* handled above so the formatted string here
+     * is always one character shorter.
+     */
+    if (strftime (start, iso8601::bufsize-1, format, &tmp) == 0)
+    {
+        return std::string ();
+    }
+
+    buf [iso8601::bufsize-1] = '\0'; /* Just in case. */
+
+    return std::string (buf);
+}
+
+typedef struct tm * (*time_breakdown_fn) (const time_t *, struct tm *);
+
+/**
+ * @brief         Format a UNIX timestamp according to ISO-8601. Converts
+ *                to broken down time first.
+ *
+ * @param t       Time to format as broken-down \c struct tm.
+ * @param date    Include the day part ([-]YYYY-MM-DD).
+ * @param time    Include the time part (hh:mm:ss).
+ * @param tz      Include the timezone ([±]ZZZZ); only heeded if
+ *                \c time is requested as well.
+ *
+ * @return        The formatted timestamp.
+ */
+std::string format_iso8601 (time_t t, const bool utc, const bool date,
+                            const bool time, const bool tz)
+{
+    time_breakdown_fn breakdown = utc ? gmtime_r : localtime_r;
+    struct tm tm;
+
+    errno = 0;
+    if (breakdown (&t, &tm) == NULL) {
+        return std::string ("error analyzing timestamp: ") + strerror (errno);
+    }
+
+    return format_iso8601 (tm, date, time, tz);
+}
+
+/**
+ * @brief         Read a ISO-8601 formatted date stamp into broken down time.
+ *
+ * @param s       String containing the timestamp.
+ *
+ * @return        \c boost::none if the input string was \c NULL or malformed,
+ *                an optional \c struct tm with the extracted values otherwise.
+ */
+boost::optional<struct tm>
+scan_iso8601 (const char *s,
+              const bool date, const bool time, const bool tz) NOEXCEPT
+{
+    struct tm tm;
+    const char *format = iso8601::pick_fmt (date, time, tz, true);
+    const char *start = s;
+    bool negyear = false;
+
+    if (s == NULL) {
+        return boost::none;
+    }
+
+    switch (s [0]) {
+        case '\0': {
+            return boost::none;
+            break;
+        }
+        /*
+         * Contrary to what the man page indicates, strptime(3) is *not*
+         * the inverse operation of strftime(3)! The later correctly formats
+         * negative year numbers with the %F modifier wheres the former trips
+         * over the sign character.
+         */
+        case '-': {
+            negyear = true;
+            start++;
+            break;
+        }
+        default: {
+            break;
+        }
+    }
+
+    memset (&tm, 0, sizeof (tm));
+
+    if (strptime (start, format, &tm) == NULL) {
+        return boost::none;
+    }
+
+    if (negyear) {
+        tm.tm_year = flip_tm_year (tm.tm_year);
+    }
+
+    return tm;
+}
+
+/**
+ * @brief         Format a \c struct timespec in the schema established by
+ *                time(1): “3m14.159s”.
+ *
+ * @param ts      The time spec to format.
+ *
+ * @return        \c boost:none in case of error during formatting, an optional
+ *                \c std::string otherwise.
+ */
+boost::optional<std::string>
+format_min_sec_msec (const struct timespec &ts)
+{
+    char ms [4] = { '\0', '\0', '\0', '\0' };
+
+    if (snprintf (ms, 4, "%.3ld", ts.tv_nsec / 1000000) < 0) {
+        return boost::none;
+    }
+
+    const time_t min = ts.tv_sec / 60;
+    const time_t sec = ts.tv_sec - min * 60;
+
+    return I2n::to_string (min) + "m"
+         + I2n::to_string (sec) + "."
+         + ms + "s"
+         ;
+}
+
+namespace I2n {
+
+namespace clock {
+
+    namespace {
+
+        static inline clockid_t
+        clockid_of_flags (const enum type::id      id,
+                          const enum type::variant var) NOEXCEPT
+        {
+            clockid_t cid = CLOCK_MONOTONIC_COARSE;
+
+            switch (id) {
+
+                default:
+                case type::mono: {
+                    switch (var) {
+                        default: {
+                            break;
+                        }
+                        case type::raw: {
+                            cid = CLOCK_MONOTONIC_RAW;
+                            break;
+                        }
+                        case type::exact: {
+                            cid = CLOCK_MONOTONIC;
+                            break;
+                        }
+                    }
+                    break;
+                }
+
+                case type::real: {
+                    if (var == type::exact) {
+                        cid = CLOCK_REALTIME;
+                    } else {
+                        cid = CLOCK_REALTIME_COARSE;
+                    }
+                    break;
+                }
+
+                case type::boot: {
+                    if (var & type::exact) {
+                        cid = CLOCK_BOOTTIME;
+                    }
+                    break;
+                }
+
+                case type::cpu: {
+                    if (var == type::thread) {
+                        cid = CLOCK_THREAD_CPUTIME_ID;
+                    } else {
+                        cid = CLOCK_PROCESS_CPUTIME_ID;
+                    }
+                    break;
+                }
+            } /* [switch id] */
+
+            return cid;
+        }
+
+        static const struct timespec zero_time = { 0, 0 };
+
+    } /* [namespace] */
+
+    Time::Time (const enum type::id id,
+                const enum type::variant var) NOEXCEPT
+        : value   (zero_time)
+        , id      (id)
+        , variant (var)
+        , err     (0)
+    { }
+
+    /*
+     * Ctor from *struct tm*. On 32 bit systems the conversion to *time_t* will
+     * fail with years outside the range from epoch to 2038.
+     */
+    Time::Time (const struct tm          &tm,
+                const enum type::id       id,
+                const enum type::variant  var)
+    {
+        struct tm tmp_tm; /* dummy for mktime(3) */
+        Time   tmp_time;
+
+        memcpy (&tmp_tm, &tm, sizeof (tmp_tm));
+
+        errno = 0;
+        const time_t t = mktime (&tmp_tm);
+        if (t == - 1) { /* Glibc does not set errno on out-of-range here! */
+            const char *datestr = asctime (&tm);
+            throw conversion_error (errno,
+                                    std::string ("mktime: from struct tm {")
+                                    + std::string (datestr, 0, strlen(datestr)-1)
+                                    + "}");
+        }
+
+        tmp_time = Time (t, 0l, id, var);
+
+        this->swap (tmp_time);
+    }
+
+    int64_t
+    Time::as_nanosec (void) const NOEXCEPT
+    {
+        return int64_t (this->value.tv_sec) * TIME_CONST_FACTOR_NANO
+             + this->value.tv_nsec;
+    }
+
+    long
+    Time::as_nanosec_L (void) const NOEXCEPT /* likely to overflow */
+    { return static_cast<long>(this->as_nanosec ()); }
+
+    Time &
+    Time::operator= (Time t2) NOEXCEPT
+    {
+        this->swap (t2);
+
+        return *this;
+    }
+
+    Time &
+    Time::operator= (struct timespec ts) NOEXCEPT
+    {
+        std::swap (this->value, ts);
+        this->id      = clock::type::mono;
+        this->variant = clock::type::dflt;
+        this->err     = 0;
+
+        return *this;
+    }
+
+    void
+    Time::unset (void) NOEXCEPT
+    { this->value = zero_time; }
+
+    bool
+    Time::set (void) NOEXCEPT
+    {
+        struct timespec now;
+
+        errno = 0;
+        if (clock_gettime (clockid_of_flags (this->id, this->variant), &now)
+            == -1)
+        {
+            this->err = errno;
+            this->unset ();
+
+            return false;
+        }
+        this->err   = 0;
+        this->value = now;
+
+        return true;
+    }
+
+    Time &
+    Time::add (const time_t sec, const long nsec) NOEXCEPT
+    {
+        this->value.tv_sec  += sec;
+        this->value.tv_nsec += nsec;
+
+        this->carry_nsec ();
+
+        return *this;
+    }
+
+    Time &
+    Time::subtract (const time_t sec, const long nsec) NOEXCEPT
+    {
+        this->value.tv_sec  -= sec;
+        this->value.tv_nsec -= nsec;
+
+        this->carry_nsec ();
+
+        return *this;
+    }
+
+    Time &
+    Time::scale (const int64_t factor) NOEXCEPT
+    {
+        this->value.tv_sec  *= factor;
+        this->value.tv_nsec *= factor;
+
+        this->carry_nsec ();
+
+        return *this;
+    }
+
+    /*
+     * Below division code purposely does not attempt to handle divide-
+     * by-zero just as any other C++ division function does. It is up to
+     * the caller to ensure that the divisor is not zero.
+     */
+    Time &
+    Time::divide (const int64_t divisor) NOEXCEPT
+    {
+        const long   sec  = static_cast<long>    (this->value.tv_sec );
+        int64_t      nsec = static_cast<int64_t> (this->value.tv_nsec);
+        const ldiv_t div  = ldiv (sec, divisor);
+
+        if (div.rem != 0) {
+            nsec += div.rem * TIME_CONST_FACTOR_NANO;
+        }
+
+        nsec /= divisor;
+
+        this->value.tv_sec  = static_cast<time_t> (div.quot);
+        this->value.tv_nsec = static_cast<long>   (nsec);
+
+        this->carry_nsec ();
+
+        return *this;
+    }
+
+    boost::optional<std::string>
+    Time::format_iso8601 (const bool utc,
+                          const bool date,
+                          const bool time,
+                          const bool tz) const
+    {
+        time_breakdown_fn breakdown = utc ? gmtime_r : localtime_r;
+        struct tm tm;
+
+        if (breakdown (&this->value.tv_sec, &tm) == NULL) {
+            return boost::none;
+        }
+
+        return ::format_iso8601 (tm, date, time, tz);
+    }
+
+    std::string
+    Time::make_nice_time (void) const
+    {
+        /* XXX the cast below results in loss of precision with 64 bit time_t! */
+        return ::make_nice_time (static_cast<int> (this->value.tv_sec));
+    }
+
+    std::string
+    Time::format_full_time (void) const
+    { return ::format_full_time (this->value.tv_sec); }
+
+    std::string
+    Time::format_date (void) const
+    { return ::format_date (this->value.tv_sec); }
+
+    boost::optional<Time>
+    now (const enum type::id id, const enum type::variant var) NOEXCEPT
+    {
+        Time ret (id, var);
+
+        if (!ret.set ()) {
+            return boost::none;
+        }
+
+        return ret;
+    }
+
+    Time
+    zero (const enum type::id id, const enum type::variant var) NOEXCEPT
+    { return Time (id, var); }
+
+    int
+    compare (const Time &t1, const Time &t2) NOEXCEPT
+    {
+        if (t1.value.tv_sec < t2.value.tv_sec) {
+            return -1;
+        }
+
+        if (t1.value.tv_sec > t2.value.tv_sec) {
+            return 1;
+        }
+
+        if (t1.value.tv_nsec < t2.value.tv_nsec) {
+            return -1;
+        }
+
+        if (t1.value.tv_nsec > t2.value.tv_nsec) {
+            return 1;
+        }
+
+        return 0;
+    }
+
+    boost::optional<Time>
+    time_of_iso8601 (const std::string        &s,
+                     const bool                date,
+                     const bool                time,
+                     const bool                tz,
+                     const enum type::id       id,
+                     const enum type::variant  var) NOEXCEPT
+    {
+        boost::optional<struct tm> tm = scan_iso8601 (s, date, time, tz);
+
+        if (!tm) {
+            return boost::none;
+        }
+
+        try {
+            return Time (*tm, id, var);
+        }
+        catch (conversion_error &_unused) { }
+
+        return boost::none;
+    }
+
+} /* [namespace clock] */
+
+} /* [namespace I2n] */