changeset 14586:21d67121985a

Adds ISO8601/RFC3339 date format parsing and construction support. Interface is somewhat based on message date parser in src/lib-mail, but it also provides access to struct tm.
author Stephan Bosch <stephan@rename-it.nl>
date Sat, 02 Jun 2012 16:55:21 +0300
parents 8bb23c123ea3
children ba36e4380cf4
files src/lib/Makefile.am src/lib/iso8601-date.c src/lib/iso8601-date.h src/lib/test-iso8601-date.c src/lib/test-lib.c src/lib/test-lib.h
diffstat 6 files changed, 476 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/src/lib/Makefile.am	Tue May 22 23:19:16 2012 +0300
+++ b/src/lib/Makefile.am	Sat Jun 02 16:55:21 2012 +0300
@@ -52,6 +52,7 @@
 	ipwd.c \
 	iostream.c \
 	iostream-rawlog.c \
+	iso8601-date.c \
 	istream.c \
 	istream-base64-encoder.c \
 	istream-concat.c \
@@ -171,6 +172,7 @@
 	iostream-private.h \
 	iostream-rawlog.h \
 	iostream-rawlog-private.h \
+	iso8601-date.h \
 	istream.h \
 	istream-base64-encoder.h \
 	istream-concat.h \
@@ -252,6 +254,7 @@
 	test-crc32.c \
 	test-hash-format.c \
 	test-hex-binary.c \
+	test-iso8601-date.c \
 	test-istream-base64-encoder.c \
 	test-istream-concat.c \
 	test-istream-crlf.c \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/iso8601-date.c	Sat Jun 02 16:55:21 2012 +0300
@@ -0,0 +1,305 @@
+#include "lib.h"
+#include "utc-offset.h"
+#include "utc-mktime.h"
+#include "iso8601-date.h"
+
+#include <ctype.h>
+
+/* RFC3339/ISO8601 date-time syntax
+
+   date-fullyear   = 4DIGIT
+   date-month      = 2DIGIT  ; 01-12
+   date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on
+                             ; month/year
+   time-hour       = 2DIGIT  ; 00-23
+   time-minute     = 2DIGIT  ; 00-59
+   time-second     = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second
+                             ; rules
+   time-secfrac    = "." 1*DIGIT
+   time-numoffset  = ("+" / "-") time-hour ":" time-minute
+   time-offset     = "Z" / time-numoffset
+
+   partial-time    = time-hour ":" time-minute ":" time-second [time-secfrac]
+   full-date       = date-fullyear "-" date-month "-" date-mday
+   full-time       = partial-time time-offset
+
+   date-time       = full-date "T" full-time
+ */
+
+struct iso8601_date_parser {
+	const unsigned char *cur, *end;
+
+	struct tm tm;
+	int timezone_offset;
+};
+
+static inline int
+iso8601_date_parse_number(struct iso8601_date_parser *parser,
+			  int digits, int *number_r)
+{
+	int i;
+
+	if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
+		return 0;
+
+	*number_r = parser->cur[0] - '0';
+	parser->cur++;
+
+	for (i=0; i < digits-1; i++) {
+		if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
+			return -1;
+		*number_r = ((*number_r) * 10) + parser->cur[0] - '0';
+		parser->cur++;
+	}
+	return 1;
+}
+
+static int
+iso8601_date_parse_secfrac(struct iso8601_date_parser *parser)
+{
+	/* time-secfrac    = "." 1*DIGIT
+
+	   NOTE: Currently not applied anywhere, so fraction is just skipped.
+	*/
+
+	/* "." */
+	if (parser->cur >= parser->end || parser->cur[0] != '.')
+		return 0;
+	parser->cur++;
+
+	/* 1DIGIT */
+	if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
+		return -1;
+	parser->cur++;
+
+	/* *DIGIT */
+	while (parser->cur < parser->end && i_isdigit(parser->cur[0]))
+		parser->cur++;
+	return 1;
+}
+
+static int is08601_date_parse_time_offset(struct iso8601_date_parser *parser)
+{
+	int tz_sign = 1, tz_hour = 0, tz_min = 0;
+	
+	/* time-offset     = "Z" / time-numoffset
+	   time-numoffset  = ("+" / "-") time-hour ":" time-minute 
+	   time-hour       = 2DIGIT  ; 00-23
+	   time-minute     = 2DIGIT  ; 00-59
+	 */
+
+	if (parser->cur >= parser->end)
+		return 0;
+
+	/* time-offset = "Z" / time-numoffset */
+	switch (parser->cur[0]) {
+	case '-':
+		tz_sign = -1;
+
+	case '+':
+		parser->cur++;
+
+		/* time-hour = 2DIGIT */
+		if (iso8601_date_parse_number(parser, 2, &tz_hour) <= 0)
+			return -1;
+		if (tz_hour > 23)
+			return -1;
+
+		/* ":" */
+		if (parser->cur >= parser->end || parser->cur[0] != ':')
+			return -1;
+		parser->cur++;
+
+		/* time-minute = 2DIGIT */
+		if (iso8601_date_parse_number(parser, 2, &tz_min) <= 0)
+			return -1;
+		if (tz_min > 59)
+			return -1;
+		break;
+	case 'Z':
+	case 'z':
+		parser->cur++;
+		break;
+	default:
+		return -1;
+	}
+
+	parser->timezone_offset = tz_sign*(tz_hour*60 + tz_min);
+	return 1;
+}
+
+static int is08601_date_parse_full_time(struct iso8601_date_parser *parser)
+{
+	/* full-time       = partial-time time-offset
+	   partial-time    = time-hour ":" time-minute ":" time-second [time-secfrac]	   
+	   time-hour       = 2DIGIT  ; 00-23
+	   time-minute     = 2DIGIT  ; 00-59
+	   time-second     = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second
+	                             ; rules
+	 */
+
+	/* time-hour = 2DIGIT */
+	if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_hour) <= 0)
+		return -1;
+
+	/* ":" */
+	if (parser->cur >= parser->end || parser->cur[0] != ':')
+		return -1;
+	parser->cur++;
+
+	/* time-minute = 2DIGIT */
+	if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_min) <= 0)
+		return -1;
+
+	/* ":" */
+	if (parser->cur >= parser->end || parser->cur[0] != ':')
+		return -1;
+	parser->cur++;
+
+	/* time-second = 2DIGIT */
+	if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_sec) <= 0)
+		return -1;
+
+	/* [time-secfrac] */
+	if (iso8601_date_parse_secfrac(parser) < 0)
+		return -1;
+
+	/* time-offset */
+	if (is08601_date_parse_time_offset(parser) <= 0)
+		return -1;
+	return 1;
+}
+
+static int is08601_date_parse_full_date(struct iso8601_date_parser *parser)
+{
+	/* full-date       = date-fullyear "-" date-month "-" date-mday
+	   date-fullyear   = 4DIGIT
+	   date-month      = 2DIGIT  ; 01-12
+	   date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on
+	                             ; month/year
+	 */
+	
+	/* date-fullyear = 4DIGIT */
+	if (iso8601_date_parse_number(parser, 4, &parser->tm.tm_year) <= 0)
+		return -1;
+	if (parser->tm.tm_year < 1900)
+		return -1;
+	parser->tm.tm_year -= 1900;
+
+	/* "-" */
+	if (parser->cur >= parser->end || parser->cur[0] != '-')
+		return -1;
+	parser->cur++;
+
+	/* date-month = 2DIGIT */
+	if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_mon) <= 0)
+		return -1;
+	parser->tm.tm_mon -= 1;
+
+	/* "-" */
+	if (parser->cur >= parser->end || parser->cur[0] != '-')
+		return -1;
+	parser->cur++;
+
+	/* time-second = 2DIGIT */
+	if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_mday) <= 0)
+		return -1;
+	return 1;
+}
+
+static int iso8601_date_parse_date_time(struct iso8601_date_parser *parser)
+{
+	/* date-time       = full-date "T" full-time */
+
+	/* full-date */
+	if (is08601_date_parse_full_date(parser) <= 0)
+		return -1;
+
+	/* "T" */
+	if (parser->cur >= parser->end ||
+	    (parser->cur[0] != 'T' && parser->cur[0] != 't'))
+		return -1;
+	parser->cur++;
+
+	/* full-time */
+	if (is08601_date_parse_full_time(parser) <= 0)
+		return -1;
+
+	if (parser->cur != parser->end)
+		return -1;
+	return 1;
+}
+
+static bool
+iso8601_date_do_parse(const unsigned char *data, size_t size, struct tm *tm_r,
+		      time_t *timestamp_r, int *timezone_offset_r)
+{
+	struct iso8601_date_parser parser;
+	time_t timestamp;
+
+	memset(&parser, 0, sizeof(parser));
+	parser.cur = data;
+	parser.end = data + size;
+
+	if (iso8601_date_parse_date_time(&parser) <= 0)
+		return FALSE;
+
+	parser.tm.tm_isdst = -1;
+	timestamp = utc_mktime(&parser.tm);
+	if (timestamp == (time_t)-1)
+		return FALSE;
+
+	if (timezone_offset_r != NULL)
+		*timezone_offset_r = parser.timezone_offset;
+	if (tm_r != NULL)
+		*tm_r = parser.tm;
+	if (timestamp_r != NULL)
+		*timestamp_r = timestamp - parser.timezone_offset * 60;
+	return TRUE;
+}
+
+bool iso8601_date_parse(const unsigned char *data, size_t size,
+			time_t *timestamp_r, int *timezone_offset_r)
+{
+	return iso8601_date_do_parse(data, size, NULL,
+				     timestamp_r, timezone_offset_r);
+}
+
+bool iso8601_date_parse_tm(const unsigned char *data, size_t size,
+			   struct tm *tm_r, int *timezone_offset_r)
+{
+	return iso8601_date_do_parse(data, size, tm_r, NULL, timezone_offset_r);
+}
+
+const char *iso8601_date_create_tm(struct tm *tm, int timezone_offset)
+{
+	const char *time_offset;
+
+	if (timezone_offset == INT_MAX)
+		time_offset = "Z";
+	else {
+		char sign = '+';
+		if (timezone_offset < 0) {
+			timezone_offset = -timezone_offset;
+			sign = '-';
+		} 
+		time_offset = t_strdup_printf("%c%02d:%02d", sign,
+					      timezone_offset / 60,
+					      timezone_offset % 60);
+	}
+
+	return t_strdup_printf("%04d-%02d-%02dT%02d:%02d:%02d%s",
+			tm->tm_year + 1900, tm->tm_mon+1, tm->tm_mday,
+			tm->tm_hour, tm->tm_min, tm->tm_sec, time_offset);
+}
+
+const char *iso8601_date_create(time_t timestamp)
+{
+	struct tm *tm;
+	int timezone_offset;
+
+	tm = localtime(&timestamp);
+	timezone_offset = utc_offset(tm, timestamp);
+	
+	return iso8601_date_create_tm(tm, timezone_offset);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/iso8601-date.h	Sat Jun 02 16:55:21 2012 +0300
@@ -0,0 +1,21 @@
+#ifndef ISO8601_DATE_H
+#define ISO8601_DATE_H
+
+/* Parses ISO8601 (RFC3339) date-time string. timezone_offset is filled with the
+   timezone's difference to UTC in minutes. Returned time_t timestamp is
+   compensated for time zone. */
+bool iso8601_date_parse(const unsigned char *data, size_t size,
+			time_t *timestamp_r, int *timezone_offset_r);
+/* Equal to iso8601_date_parse, but writes uncompensated timestamp to tm_r. */
+bool iso8601_date_parse_tm(const unsigned char *data, size_t size,
+			   struct tm *tm_r, int *timezone_offset_r);
+
+/* Create ISO8601 date-time string from given time struct in specified
+   timezone. A zone offset of zero will not to 'Z', but '+00:00'. If
+   zone_offset == INT_MAX, the time zone will be 'Z'. */
+const char *iso8601_date_create_tm(struct tm *tm, int zone_offset);
+
+/* Create ISO8601 date-time string from given time in local timezone. */
+const char *iso8601_date_create(time_t timestamp);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/test-iso8601-date.c	Sat Jun 02 16:55:21 2012 +0300
@@ -0,0 +1,145 @@
+/* Copyright (c) 2009-2011 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "test-common.h"
+#include "iso8601-date.h"
+
+#include <time.h>
+
+struct iso8601_date_test {
+	const char *date_in;
+	const char *date_out;
+
+	struct tm tm;
+	int zone_offset;
+};
+
+/* Valid date tests */
+struct iso8601_date_test valid_date_tests[] = {
+	{ 
+		.date_in = "2007-11-07T23:05:34+00:00",
+		.tm = {
+			.tm_year = 107, .tm_mon = 10, .tm_mday = 7,
+			.tm_hour = 23, .tm_min = 5, .tm_sec = 34 },
+	},{ 
+		.date_in = "2011-01-07T21:03:31+00:30",
+		.tm = {
+			.tm_year = 111, .tm_mon = 0, .tm_mday = 7,
+			.tm_hour = 21, .tm_min = 3, .tm_sec = 31 },
+		.zone_offset = 30
+	},{ 
+		.date_in = "2006-05-09T18:04:12+05:30",
+		.tm = {
+			.tm_year = 106, .tm_mon = 4, .tm_mday = 9,
+			.tm_hour = 18, .tm_min = 4, .tm_sec = 12 },
+		.zone_offset = 5*60+30
+	},{ 
+		.date_in = "1975-10-30T06:33:29Z",
+		.date_out = "1975-10-30T06:33:29+00:00",
+		.tm = {
+			.tm_year = 75, .tm_mon = 9, .tm_mday = 30,
+			.tm_hour = 6, .tm_min = 33, .tm_sec = 29 },
+	},{ 
+		.date_in = "1988-04-24t15:02:12z",
+		.date_out = "1988-04-24T15:02:12+00:00",
+		.tm = {
+			.tm_year = 88, .tm_mon = 3, .tm_mday = 24,
+			.tm_hour = 15, .tm_min = 2, .tm_sec = 12 },
+	},{ 
+		.date_in = "2012-02-29T08:12:34.23198Z",
+		.date_out = "2012-02-29T08:12:34+00:00",
+		.tm = {
+			.tm_year = 112, .tm_mon = 1, .tm_mday = 29,
+			.tm_hour = 8, .tm_min = 12, .tm_sec = 34 },
+	}
+};
+
+unsigned int valid_date_test_count = N_ELEMENTS(valid_date_tests);
+
+static void test_iso8601_date_valid(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < valid_date_test_count; i++) T_BEGIN {
+		const char *date_in, *date_out, *pdate_out;
+		struct tm *tm = &valid_date_tests[i].tm, ptm;
+		int zone_offset = valid_date_tests[i].zone_offset, pzone_offset;
+		bool result;
+
+		date_in = valid_date_tests[i].date_in;
+		date_out = valid_date_tests[i].date_out == NULL ?
+			date_in : valid_date_tests[i].date_out;
+
+		test_begin(t_strdup_printf("iso8601 date valid [%d]", i));
+
+		result = iso8601_date_parse_tm
+			((const unsigned char *)date_in, strlen(date_in), &ptm, &pzone_offset);
+		test_out(t_strdup_printf("parse %s", date_in), result);
+		if (result) {
+			bool equal = tm->tm_year == ptm.tm_year && tm->tm_mon == ptm.tm_mon &&
+				tm->tm_mday == ptm.tm_mday && tm->tm_hour == ptm.tm_hour &&
+				tm->tm_min == ptm.tm_min && tm->tm_sec == ptm.tm_sec;
+
+			test_out("valid timestamp", equal);
+			test_out_reason("valid timezone", zone_offset == pzone_offset,
+				t_strdup_printf("%d", pzone_offset));
+
+			pdate_out = iso8601_date_create_tm(tm, zone_offset);
+			test_out_reason("valid create", strcmp(date_out, pdate_out) == 0,
+				pdate_out);
+		}
+
+		test_end();
+	} T_END;
+}
+
+/* Invalid date tests */
+const char *invalid_date_tests[] = {
+	"200-11-17T23:05:34+00:00",
+	"2007:11-17T23:05:34+00:00",
+	"2007-11?17T23:05:34+00:00",
+	"2007-49-17T23:05:34+00:00",
+	"2007-11-77T23:05:34+00:00",
+	"2007-11-17K23:05:34+00:00",
+	"2007-11-13T59:05:34+00:00",
+	"2007-112-13T12:15:34+00:00",
+	"2007-11-133T12:15:34+00:00",
+	"2007-11-13T12J15:34+00:00",
+	"2007-11-13T12:15*34+00:00",
+	"2007-11-13T12:15:34/00:00",
+	"2007-11-13T12:15:34+00-00",
+	"2007-11-13T123:15:34+00:00",
+	"2007-11-13T12:157:34+00:00",
+	"2007-11-13T12:15:342+00:00",
+	"2007-11-13T12:15:34+001:00",
+	"2007-11-13T12:15:32+00:006",
+	"2007-02-29T15:13:21Z"
+};
+
+unsigned int invalid_date_test_count = N_ELEMENTS(invalid_date_tests);
+
+static void test_iso8601_date_invalid(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < invalid_date_test_count; i++) T_BEGIN {
+		const char *date_in;
+		bool result;
+
+		date_in = invalid_date_tests[i];
+
+		test_begin(t_strdup_printf("iso8601 date invalid [%d]", i));
+
+		result = iso8601_date_parse_tm
+			((const unsigned char *)date_in, strlen(date_in), NULL, NULL);
+		test_out(t_strdup_printf("parse %s", date_in), !result);
+
+		test_end();
+	} T_END;
+}
+
+void test_iso8601_date(void)
+{
+	test_iso8601_date_valid();
+	test_iso8601_date_invalid();
+}
--- a/src/lib/test-lib.c	Tue May 22 23:19:16 2012 +0300
+++ b/src/lib/test-lib.c	Sat Jun 02 16:55:21 2012 +0300
@@ -13,6 +13,7 @@
 		test_crc32,
 		test_hash_format,
 		test_hex_binary,
+		test_iso8601_date,
 		test_istream_base64_encoder,
 		test_istream_concat,
 		test_istream_crlf,
--- a/src/lib/test-lib.h	Tue May 22 23:19:16 2012 +0300
+++ b/src/lib/test-lib.h	Sat Jun 02 16:55:21 2012 +0300
@@ -12,6 +12,7 @@
 void test_crc32(void);
 void test_hash_format(void);
 void test_hex_binary(void);
+void test_iso8601_date(void);
 void test_istream_base64_encoder(void);
 void test_istream_concat(void);
 void test_istream_crlf(void);