handle strftime() more defensively
[libi2ncommon] / src / timefunc.cpp
index 8fe5000..88e6c33 100644 (file)
@@ -24,6 +24,7 @@ on this file might be covered by the GNU General Public License.
  *
  */
 
+#include <cstdio>
 #include <errno.h>
 #include <string>
 #include <sstream>
@@ -781,45 +782,79 @@ bool realtime_clock_gettime(long int& seconds, long int& nano_seconds)
  *      - strptime(3) will not parse the leading dash with that format
  *        but apart from that it works well.
  */
-static const char *const iso8601_fmt_d    = "%4Y-%m-%d";
-static const char *const iso8601_fmt_t    = "%T";
-static const char *const iso8601_fmt_tz   = "%TZ%z";
-static const char *const iso8601_fmt_dt   = "%4Y-%m-%dT%T";
-static const char *const iso8601_fmt_dtz  = "%4Y-%m-%dT%TZ%z";
-
-static inline const char *
-pick_iso8601_fmt (const bool date, const bool time, const bool tz)
-{
-    const char *format = NULL;
 
-    if (date) {
-        if (time) {
-            if (tz) {
-                format = iso8601_fmt_dtz;
+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_fmt_dt;
+                format = iso8601::d;
             }
+        } else if (time && tz) {
+            format = iso8601::tz;
         } else {
-            format = iso8601_fmt_d;
+            format = iso8601::t; /* default to %T */
         }
-    } else if (time && tz) {
-        format = iso8601_fmt_tz;
-    } else {
-        format = iso8601_fmt_t; /* default to %T */
+
+        return table [format];
     }
 
-    return format;
-}
+} /* [iso8601] */
 
-namespace {
 
-    /*
-     * 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 iso8601_bufsize = 33;
+namespace {
 
     static inline int flip_tm_year (const int y)
     { return (y + 1900) * -1 - 1900; }
@@ -833,7 +868,7 @@ namespace {
  * @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 heeded if
+ * @param tz      Include the timezone ([±]ZZZZ); only needed if
  *                \c time is requested as well.
  *
  * @return        The formatted timestamp.
@@ -842,9 +877,9 @@ 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 buf [iso8601::bufsize] = { 0 };
     char *start = &buf [0];
-    const char *format = pick_iso8601_fmt (date, time, tz);
+    const char *format = iso8601::pick_fmt (date, time, tz);
 
     memcpy (&tmp, &tm, sizeof (tmp));
 
@@ -855,12 +890,15 @@ std::string format_iso8601 (const struct tm &tm, const bool date,
     }
 
     /*
-     * The sign is *always* handled above so the formatted string her
+     * The sign is *always* handled above so the formatted string here
      * is always one character shorter.
-     * */
-    const size_t n = strftime (start, iso8601_bufsize-1, format, &tmp);
+     */
+    if (strftime (start, iso8601::bufsize-1, format, &tmp) == 0)
+    {
+        return std::string ();
+    }
 
-    buf [n+1] = '\0';
+    buf [iso8601::bufsize-1] = '\0'; /* Just in case. */
 
     return std::string (buf);
 }
@@ -906,7 +944,7 @@ scan_iso8601 (const char *s,
               const bool date, const bool time, const bool tz) NOEXCEPT
 {
     struct tm tm;
-    const char *format = pick_iso8601_fmt (date, time, tz);
+    const char *format = iso8601::pick_fmt (date, time, tz, true);
     const char *start = s;
     bool negyear = false;
 
@@ -948,6 +986,32 @@ scan_iso8601 (const char *s,
     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 {
 
@@ -1022,15 +1086,30 @@ namespace clock {
         , 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) NOEXCEPT
+                const enum type::variant  var)
     {
         struct tm tmp_tm; /* dummy for mktime(3) */
         Time   tmp_time;
 
         memcpy (&tmp_tm, &tm, sizeof (tmp_tm));
-        tmp_time = Time (mktime (&tmp_tm), 0l, id, var);
+
+        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);
     }
@@ -1164,18 +1243,18 @@ namespace clock {
         return ::format_iso8601 (tm, date, time, tz);
     }
 
-    boost::optional<std::string>
+    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));
     }
 
-    boost::optional<std::string>
+    std::string
     Time::format_full_time (void) const
     { return ::format_full_time (this->value.tv_sec); }
 
-    boost::optional<std::string>
+    std::string
     Time::format_date (void) const
     { return ::format_date (this->value.tv_sec); }
 
@@ -1231,7 +1310,12 @@ namespace clock {
             return boost::none;
         }
 
-        return Time (*tm, id, var);
+        try {
+            return Time (*tm, id, var);
+        }
+        catch (conversion_error &_unused) { }
+
+        return boost::none;
     }
 
 } /* [namespace clock] */