/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */

#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;
		/* fall through */
	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;

	i_zero(&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;

	*timezone_offset_r = parser.timezone_offset;
	*tm_r = parser.tm;
	*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)
{
	struct tm tm;

	return iso8601_date_do_parse(data, size, &tm,
				     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)
{
	time_t timestamp;

	return iso8601_date_do_parse(data, size, tm_r,
				     &timestamp, 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);
}
