Mercurial > dovecot > core-2.2
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(×tamp); + 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);