compat: MUSL implementation of strptime()

On Windows there's no strptime(). A similar functionality is available
from C++11's `std::get_time` in `<iomanip>`, but there are subtle
differences, including actual behavioral differences when parsing
invalid dates such as "January 41st" (which is invalid everywhere, and
could become "January 4th", eating one char less), or Feb 29th, 2018
(which does not exist).

Let's just import the MIT-licensed implementation from the MUSL C
library because it's reasonably self-contained and appears to work the
same as the glibc one.
diff --git a/CMakeModules/UseCompat.cmake b/CMakeModules/UseCompat.cmake
index d81f28c..ac330e5 100644
--- a/CMakeModules/UseCompat.cmake
+++ b/CMakeModules/UseCompat.cmake
@@ -53,6 +53,7 @@
 
     check_symbol_exists(realpath "stdlib.h" HAVE_REALPATH)
     check_symbol_exists(localtime_r "time.h" HAVE_LOCALTIME_R)
+    check_symbol_exists(strptime "time.h" HAVE_STRPTIME)
 
     unset(CMAKE_REQUIRED_DEFINITIONS)
     unset(CMAKE_REQUIRED_LIBRARIES)
@@ -64,4 +65,7 @@
     if(WIN32)
         include_directories(${PROJECT_SOURCE_DIR}/compat/posix-shims)
     endif()
+    if(NOT HAVE_STRPTIME)
+        set(compatsrc ${compatsrc} ${PROJECT_SOURCE_DIR}/compat/strptime.c)
+    endif()
 endmacro()
diff --git a/compat/compat.h.in b/compat/compat.h.in
index 64c916b..6bc45fd 100644
--- a/compat/compat.h.in
+++ b/compat/compat.h.in
@@ -63,6 +63,7 @@
 #cmakedefine HAVE_TIME_H_TIMEZONE
 #cmakedefine HAVE_REALPATH
 #cmakedefine HAVE_LOCALTIME_R
+#cmakedefine HAVE_STRPTIME
 
 #ifndef bswap64
 #define bswap64(val) \
@@ -168,4 +169,8 @@
 struct tm *localtime_r(const time_t *timep, struct tm *result);
 #endif
 
+#ifndef HAVE_STRPTIME
+char *strptime(const char *s, const char *format, struct tm *tm);
+#endif
+
 #endif /* _COMPAT_H_ */
diff --git a/compat/strptime.c b/compat/strptime.c
new file mode 100644
index 0000000..3177b92
--- /dev/null
+++ b/compat/strptime.c
@@ -0,0 +1,213 @@
+/*
+ * This comes from the musl C library which has been licensed under the permissive MIT license.
+ *
+ * Downloaded from https://git.musl-libc.org/cgit/musl/plain/src/time/strptime.c,
+ * commit 98e688a9da5e7b2925dda17a2d6820dddf1fb287.
+ * */
+
+#include <stdlib.h>
+#include <langinfo.h>
+#include <time.h>
+#include <ctype.h>
+#include <stddef.h>
+#include <string.h>
+#include <strings.h>
+
+char *strptime(const char *restrict s, const char *restrict f, struct tm *restrict tm)
+{
+	int i, w, neg, adj, min, range, *dest, dummy;
+	const char *ex;
+	size_t len;
+	int want_century = 0, century = 0, relyear = 0;
+	while (*f) {
+		if (*f != '%') {
+			if (isspace(*f)) for (; *s && isspace(*s); s++);
+			else if (*s != *f) return 0;
+			else s++;
+			f++;
+			continue;
+		}
+		f++;
+		if (*f == '+') f++;
+		if (isdigit(*f)) {
+			char *new_f;
+			w=strtoul(f, &new_f, 10);
+			f = new_f;
+		} else {
+			w=-1;
+		}
+		adj=0;
+		switch (*f++) {
+		case 'a': case 'A':
+			dest = &tm->tm_wday;
+			min = ABDAY_1;
+			range = 7;
+			goto symbolic_range;
+		case 'b': case 'B': case 'h':
+			dest = &tm->tm_mon;
+			min = ABMON_1;
+			range = 12;
+			goto symbolic_range;
+		case 'c':
+			s = strptime(s, nl_langinfo(D_T_FMT), tm);
+			if (!s) return 0;
+			break;
+		case 'C':
+			dest = &century;
+			if (w<0) w=2;
+			want_century |= 2;
+			goto numeric_digits;
+		case 'd': case 'e':
+			dest = &tm->tm_mday;
+			min = 1;
+			range = 31;
+			goto numeric_range;
+		case 'D':
+			s = strptime(s, "%m/%d/%y", tm);
+			if (!s) return 0;
+			break;
+		case 'H':
+			dest = &tm->tm_hour;
+			min = 0;
+			range = 24;
+			goto numeric_range;
+		case 'I':
+			dest = &tm->tm_hour;
+			min = 1;
+			range = 12;
+			goto numeric_range;
+		case 'j':
+			dest = &tm->tm_yday;
+			min = 1;
+			range = 366;
+			adj = 1;
+			goto numeric_range;
+		case 'm':
+			dest = &tm->tm_mon;
+			min = 1;
+			range = 12;
+			adj = 1;
+			goto numeric_range;
+		case 'M':
+			dest = &tm->tm_min;
+			min = 0;
+			range = 60;
+			goto numeric_range;
+		case 'n': case 't':
+			for (; *s && isspace(*s); s++);
+			break;
+		case 'p':
+			ex = nl_langinfo(AM_STR);
+			len = strlen(ex);
+			if (!strncasecmp(s, ex, len)) {
+				tm->tm_hour %= 12;
+				s += len;
+				break;
+			}
+			ex = nl_langinfo(PM_STR);
+			len = strlen(ex);
+			if (!strncasecmp(s, ex, len)) {
+				tm->tm_hour %= 12;
+				tm->tm_hour += 12;
+				s += len;
+				break;
+			}
+			return 0;
+		case 'r':
+			s = strptime(s, nl_langinfo(T_FMT_AMPM), tm);
+			if (!s) return 0;
+			break;
+		case 'R':
+			s = strptime(s, "%H:%M", tm);
+			if (!s) return 0;
+			break;
+		case 'S':
+			dest = &tm->tm_sec;
+			min = 0;
+			range = 61;
+			goto numeric_range;
+		case 'T':
+			s = strptime(s, "%H:%M:%S", tm);
+			if (!s) return 0;
+			break;
+		case 'U':
+		case 'W':
+			/* Throw away result, for now. (FIXME?) */
+			dest = &dummy;
+			min = 0;
+			range = 54;
+			goto numeric_range;
+		case 'w':
+			dest = &tm->tm_wday;
+			min = 0;
+			range = 7;
+			goto numeric_range;
+		case 'x':
+			s = strptime(s, nl_langinfo(D_FMT), tm);
+			if (!s) return 0;
+			break;
+		case 'X':
+			s = strptime(s, nl_langinfo(T_FMT), tm);
+			if (!s) return 0;
+			break;
+		case 'y':
+			dest = &relyear;
+			w = 2;
+			want_century |= 1;
+			goto numeric_digits;
+		case 'Y':
+			dest = &tm->tm_year;
+			if (w<0) w=4;
+			adj = 1900;
+			want_century = 0;
+			goto numeric_digits;
+		case '%':
+			if (*s++ != '%') return 0;
+			break;
+		default:
+			return 0;
+		numeric_range:
+			if (!isdigit(*s)) return 0;
+			*dest = 0;
+			for (i=1; i<=min+range && isdigit(*s); i*=10)
+				*dest = *dest * 10 + *s++ - '0';
+			if (*dest - min >= (unsigned)range) return 0;
+			*dest -= adj;
+			switch((char *)dest - (char *)tm) {
+			case offsetof(struct tm, tm_yday):
+				;
+			}
+			goto update;
+		numeric_digits:
+			neg = 0;
+			if (*s == '+') s++;
+			else if (*s == '-') neg=1, s++;
+			if (!isdigit(*s)) return 0;
+			for (*dest=i=0; i<w && isdigit(*s); i++)
+				*dest = *dest * 10 + *s++ - '0';
+			if (neg) *dest = -*dest;
+			*dest -= adj;
+			goto update;
+		symbolic_range:
+			for (i=2*range-1; i>=0; i--) {
+				ex = nl_langinfo(min+i);
+				len = strlen(ex);
+				if (strncasecmp(s, ex, len)) continue;
+				s += len;
+				*dest = i % range;
+				break;
+			}
+			if (i<0) return 0;
+			goto update;
+		update:
+			//FIXME
+			;
+		}
+	}
+	if (want_century) {
+		tm->tm_year = relyear;
+		if (want_century & 2) tm->tm_year += century * 100 - 1900;
+		else if (tm->tm_year <= 68) tm->tm_year += 100;
+	}
+	return (char *)s;
+}