/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) 1998 - 2004, Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at http://curl.haxx.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 * $Id: parsedate.c,v 1.9 2004/09/20 13:21:48 bagder Exp $
 ***************************************************************************/
/*
  A brief summary of the date string formats this parser groks:

  RFC 2616 3.3.1

  Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
  Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
  Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format

  we support dates without week day name:

  06 Nov 1994 08:49:37 GMT
  06-Nov-94 08:49:37 GMT
  Nov  6 08:49:37 1994

  without the time zone:

  06 Nov 1994 08:49:37
  06-Nov-94 08:49:37

  weird order:

  1994 Nov 6 08:49:37  (GNU date fails)
  GMT 08:49:37 06-Nov-94 Sunday
  94 6 Nov 08:49:37    (GNU date fails)

  time left out:

  1994 Nov 6
  06-Nov-94
  Sun Nov 6 94

  unusual separators:

  1994.Nov.6
  Sun/Nov/6/94/GMT

  commonly used time zone names:

  Sun, 06 Nov 1994 08:49:37 CET
  06 Nov 1994 08:49:37 EST

  time zones specified using RFC822 style:

  Sun, 12 Sep 2004 15:05:58 -0700
  Sat, 11 Sep 2004 21:32:11 +0200

  compact numerical date strings:

  20040912 15:05:58 -0700
  20040911 +0200

*/
#include "setup.h"
#include <stdio.h>
#include <ctype.h>
#include <string.h>

#ifdef HAVE_STDLIB_H
#include <stdlib.h> /* for strtol() */
#endif

#include <curl/curl.h>

static time_t Curl_parsedate( const char *date );

static const char *wkday[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
static const char *weekday[] = { "Monday", "Tuesday", "Wednesday", "Thursday",
								 "Friday", "Saturday", "Sunday" };
static const char *month[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
							   "Aug", "Sep", "Oct", "Nov", "Dec" };

struct tzinfo {
	const char *name;
	int offset; /* +/- in minutes */
};

/* Here's a bunch of frequently used time zone names. These were supported
   by the old getdate parser. */
static const struct tzinfo tz[] = {
	{"GMT", 0},  /* Greenwich Mean */
	{"UTC", 0},  /* Universal (Coordinated) */
	{"WET", 0},  /* Western European */
	{"BST", 0},  /* British Summer */
	{"WAT", 60}, /* West Africa */
	{"AST", 240}, /* Atlantic Standard */
	{"ADT", 240}, /* Atlantic Daylight */
	{"EST", 300}, /* Eastern Standard */
	{"EDT", 300}, /* Eastern Daylight */
	{"CST", 360}, /* Central Standard */
	{"CDT", 360}, /* Central Daylight */
	{"MST", 420}, /* Mountain Standard */
	{"MDT", 420}, /* Mountain Daylight */
	{"PST", 480}, /* Pacific Standard */
	{"PDT", 480}, /* Pacific Daylight */
	{"YST", 540}, /* Yukon Standard */
	{"YDT", 540}, /* Yukon Daylight */
	{"HST", 600}, /* Hawaii Standard */
	{"HDT", 600}, /* Hawaii Daylight */
	{"CAT", 600}, /* Central Alaska */
	{"AHST", 600}, /* Alaska-Hawaii Standard */
	{"NT",  660}, /* Nome */
	{"IDLW", 720}, /* International Date Line West */
	{"CET", -60}, /* Central European */
	{"MET", -60}, /* Middle European */
	{"MEWT", -60}, /* Middle European Winter */
	{"MEST", -120}, /* Middle European Summer */
	{"CEST", -120}, /* Central European Summer */
	{"MESZ", -60}, /* Middle European Summer */
	{"FWT", -60}, /* French Winter */
	{"FST", -60}, /* French Summer */
	{"EET", -120}, /* Eastern Europe, USSR Zone 1 */
	{"WAST", -420}, /* West Australian Standard */
	{"WADT", -420}, /* West Australian Daylight */
	{"CCT", -480}, /* China Coast, USSR Zone 7 */
	{"JST", -540}, /* Japan Standard, USSR Zone 8 */
	{"EAST", -600}, /* Eastern Australian Standard */
	{"EADT", -600}, /* Eastern Australian Daylight */
	{"GST", -600}, /* Guam Standard, USSR Zone 9 */
	{"NZT", -720}, /* New Zealand */
	{"NZST", -720}, /* New Zealand Standard */
	{"NZDT", -720}, /* New Zealand Daylight */
	{"IDLE", -720}, /* International Date Line East */
};

/* returns:
   -1 no day
   0 monday - 6 sunday
*/

static int checkday( char *check, size_t len ) {
	int i;
	const char **what;
	bool found = FALSE;
	if ( len > 3 ) {
		what = &weekday[0];
	} else {
		what = &wkday[0];
	}
	for ( i = 0; i < 7; i++ ) {
		if ( curl_strequal( check, what[0] ) ) {
			found = TRUE;
			break;
		}
		what++;
	}
	return found ? i : -1;
}

static int checkmonth( char *check ) {
	int i;
	const char **what;
	bool found = FALSE;

	what = &month[0];
	for ( i = 0; i < 12; i++ ) {
		if ( curl_strequal( check, what[0] ) ) {
			found = TRUE;
			break;
		}
		what++;
	}
	return found ? i : -1; /* return the offset or -1, no real offset is -1 */
}

/* return the time zone offset between GMT and the input one, in number
   of seconds or -1 if the timezone wasn't found/legal */

static int checktz( char *check ) {
	unsigned int i;
	const struct tzinfo *what;
	bool found = FALSE;

	what = tz;
	for ( i = 0; i < sizeof( tz ) / sizeof( tz[0] ); i++ ) {
		if ( curl_strequal( check, what->name ) ) {
			found = TRUE;
			break;
		}
		what++;
	}
	return found ? what->offset * 60 : -1;
}

static void skip( const char **date ) {
	/* skip everything that aren't letters or digits */
	while ( **date && !isalnum( (int)**date ) )
		( *date )++;
}

enum assume {
	DATE_MDAY,
	DATE_YEAR,
	DATE_TIME
};

static time_t Curl_parsedate( const char *date ) {
	time_t t = 0;
	int wdaynum = -1; /* day of the week number, 0-6 (mon-sun) */
	int monnum = -1; /* month of the year number, 0-11 */
	int mdaynum = -1; /* day of month, 1 - 31 */
	int hournum = -1;
	int minnum = -1;
	int secnum = -1;
	int yearnum = -1;
	int tzoff = -1;
	struct tm tm;
	enum assume dignext = DATE_MDAY;
	const char *indate = date; /* save the original pointer */

	int part = 0; /* max 6 parts */

	while ( *date && ( part < 6 ) ) {
		bool found = FALSE;

		skip( &date );

		if ( isalpha( (int)*date ) ) {
			/* a name coming up */
			char buf[32] = "";
			size_t len;
			sscanf( date, "%31[A-Za-z]", buf );
			len = strlen( buf );

			if ( wdaynum == -1 ) {
				wdaynum = checkday( buf, len );
				if ( wdaynum != -1 ) {
					found = TRUE;
				}
			}
			if ( !found && ( monnum == -1 ) ) {
				monnum = checkmonth( buf );
				if ( monnum != -1 ) {
					found = TRUE;
				}
			}

			if ( !found && ( tzoff == -1 ) ) {
				/* this just must be a time zone string */
				tzoff = checktz( buf );
				if ( tzoff != -1 ) {
					found = TRUE;
				}
			}

			if ( !found ) {
				return -1; /* bad string */

			}
			date += len;
		} else if ( isdigit( (int)*date ) )        {
			/* a digit */
			int val;
			char *end;
			if ( ( secnum == -1 ) &&
				 ( 3 == sscanf( date, "%02d:%02d:%02d", &hournum, &minnum, &secnum ) ) ) {
				/* time stamp! */
				date += 8;
				found = TRUE;
			} else {
				val = (int)strtol( date, &end, 10 );

				if ( ( tzoff == -1 ) &&
					 ( ( end - date ) == 4 ) &&
					 ( val < 1300 ) &&
					 ( indate < date ) &&
					 ( ( date[-1] == '+' || date[-1] == '-' ) ) ) {
					/* four digits and a value less than 1300 and it is preceeded with
					   a plus or minus. This is a time zone indication. */
					found = TRUE;
					tzoff = ( val / 100 * 60 + val % 100 ) * 60;

					/* the + and - prefix indicates the local time compared to GMT,
					   this we need ther reversed math to get what we want */
					tzoff = date[-1] == '+' ? -tzoff : tzoff;
				}

				if ( ( ( end - date ) == 8 ) &&
					 ( yearnum == -1 ) &&
					 ( monnum == -1 ) &&
					 ( mdaynum == -1 ) ) {
					/* 8 digits, no year, month or day yet. This is YYYYMMDD */
					found = TRUE;
					yearnum = val / 10000;
					monnum = ( val % 10000 ) / 100 - 1; /* month is 0 - 11 */
					mdaynum = val % 100;
				}

				if ( !found && ( dignext == DATE_MDAY ) && ( mdaynum == -1 ) ) {
					if ( ( val > 0 ) && ( val < 32 ) ) {
						mdaynum = val;
						found = TRUE;
					}
					dignext = DATE_YEAR;
				}

				if ( !found && ( dignext == DATE_YEAR ) && ( yearnum == -1 ) ) {
					yearnum = val;
					found = TRUE;
					if ( yearnum < 1900 ) {
						if ( yearnum > 70 ) {
							yearnum += 1900;
						} else {
							yearnum += 2000;
						}
					}
					if ( mdaynum == -1 ) {
						dignext = DATE_MDAY;
					}
				}

				if ( !found ) {
					return -1;
				}

				date = end;
			}
		}

		part++;
	}

	if ( -1 == secnum ) {
		secnum = minnum = hournum = 0; /* no time, make it zero */

	}
	if ( ( -1 == mdaynum ) ||
		 ( -1 == monnum ) ||
		 ( -1 == yearnum ) ) {
		/* lacks vital info, fail */
		return -1;
	}

	tm.tm_sec = secnum;
	tm.tm_min = minnum;
	tm.tm_hour = hournum;
	tm.tm_mday = mdaynum;
	tm.tm_mon = monnum;
	tm.tm_year = yearnum - 1900;
	tm.tm_wday = 0;
	tm.tm_yday = 0;
	tm.tm_isdst = 0;

	t = mktime( &tm );

	/* time zone adjust */
	{
		struct tm *gmt;
		long delta;
		time_t t2;

#ifdef HAVE_GMTIME_R
		/* thread-safe version */
		struct tm keeptime2;
		gmt = (struct tm *)gmtime_r( &t, &keeptime2 );
#else
		gmt = gmtime( &t ); /* use gmtime_r() if available */
#endif

		t2 = mktime( gmt );

		/* Add the time zone diff (between the given timezone and GMT) and the
		   diff between the local time zone and GMT. */
		delta = ( tzoff != -1 ? tzoff : 0 ) + ( t - t2 );

		if ( ( delta > 0 ) && ( t + delta < t ) ) {
			return -1; /* time_t overflow */

		}
		t += delta;
	}

	return t;
}

time_t curl_getdate( const char *p, const time_t *now ) {
	(void)now;
	return Curl_parsedate( p );
}
