/* * Vzic - a program to convert Olson timezone database files into VZTIMEZONE * files compatible with the iCalendar specification (RFC2445). * * Copyright (C) 2000-2001 Ximian, Inc. * Copyright (C) 2003 Damon Chaplin. * * Author: Damon Chaplin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ #include #include #include #include #include "vzic.h" #include "vzic-parse.h" /* This is the maximum line length we allow. */ #define MAX_LINE_LEN 1024 /* The maximum number of fields on a line. */ #define MAX_FIELDS 12 typedef enum { ZONE_ID = 0, /* The 'Zone' at the start of the line. */ ZONE_NAME = 1, ZONE_GMTOFF = 2, ZONE_RULES_SAVE = 3, ZONE_FORMAT = 4, ZONE_UNTIL_YEAR = 5, ZONE_UNTIL_MONTH = 6, ZONE_UNTIL_DAY = 7, ZONE_UNTIL_TIME = 8 } ZoneFieldNumber; typedef enum { RULE_ID = 0, /* The 'Rule' at the start of the line. */ RULE_NAME = 1, RULE_FROM = 2, RULE_TO = 3, RULE_TYPE = 4, RULE_IN = 5, RULE_ON = 6, RULE_AT = 7, RULE_SAVE = 8, RULE_LETTER_S = 9 } RuleFieldNumber; typedef enum { LINK_ID = 0, /* The 'Link' at the start of the line. */ LINK_FROM = 1, LINK_TO = 2 } LinkFieldNumber; /* This struct contains information used while parsing the files, and is passed to most parsing functions. */ typedef struct _ParsingData ParsingData; struct _ParsingData { /* This is the line being parsed. buffer is a copy that we break into fields and sub-fields as it is parsed. */ char line[MAX_LINE_LEN]; char buffer[MAX_LINE_LEN]; /* These are pointers to the start of each field in buffer. */ char *fields[MAX_FIELDS]; int num_fields; /* These are just for producing error messages. */ char *filename; int line_number; /* This is an array of ZoneData structs, 1 for each timezone read. */ GArray *zone_data; /* This is a hash table of arrays of RuleData structs. As each Rule line is read in, a new RuleData struct is filled in and appended to the appropriate GArray in the hash table. */ GHashTable *rule_data; /* A hash containing data on the Link lines. The keys are the timezones where the link is from (i.e. the timezone we will be outputting anyway) and the data is a GList of timezones to link to (where we will copy the timezone data to). */ GHashTable *link_data; int max_until_year; }; /* * Parsing functions, used when reading the Olson timezone data file. */ static void parse_fields (ParsingData *data); static gboolean parse_zone_line (ParsingData *data); static gboolean parse_zone_continuation_line (ParsingData *data); static gboolean parse_zone_common (ParsingData *data, int offset); static void parse_rule_line (ParsingData *data); static void parse_link_line (ParsingData *data); static int parse_year (ParsingData *data, char *field, gboolean accept_only, int only_value); static int parse_month (ParsingData *data, char *field); static DayCode parse_day (ParsingData *data, char *field, int *day, int *weekday); static int parse_weekday (ParsingData *data, char *field); static int parse_time (ParsingData *data, char *field, TimeCode *time_code); static int parse_number (ParsingData *data, char **num); static int parse_rules_save (ParsingData *data, char *field, char **rules); static void parse_coord (char *coord, int len, int *result); void parse_olson_file (char *filename, GArray **zone_data, GHashTable **rule_data, GHashTable **link_data, int *max_until_year) { ParsingData data; FILE *fp; int zone_continues = 0; *zone_data = g_array_new (FALSE, FALSE, sizeof (ZoneData)); *rule_data = g_hash_table_new (g_str_hash, g_str_equal); *link_data = g_hash_table_new (g_str_hash, g_str_equal); fp = fopen (filename, "r"); if (!fp) { fprintf (stderr, "Couldn't open file: %s\n", filename); exit (1); } data.filename = filename; data.zone_data = *zone_data; data.rule_data = *rule_data; data.link_data = *link_data; data.max_until_year = 0; for (data.line_number = 0; ; data.line_number++) { if (fgets (data.line, sizeof (data.line), fp) != data.line) break; strcpy (data.buffer, data.line); parse_fields (&data); if (data.num_fields == 0) continue; if (zone_continues) { zone_continues = parse_zone_continuation_line (&data); } else if (!strcmp (data.fields[0], "Zone")) { zone_continues = parse_zone_line (&data); } else if (!strcmp (data.fields[0], "Rule")) { parse_rule_line (&data); } else if (!strcmp (data.fields[0], "Link")) { parse_link_line (&data); } else if (!strcmp (data.fields[0], "Leap")) { /* We don't care about Leap lines. */ } else { fprintf (stderr, "%s:%i: Invalid line.\n%s\n", filename, data.line_number, data.line); exit (1); } } if (ferror (fp)) { fprintf (stderr, "Error reading file: %s\n", filename); exit (1); } if (zone_continues) { fprintf (stderr, "%s:%i: Zone continuation line expected.\n%s\n", filename, data.line_number, data.line); exit (1); } fclose (fp); #if 0 printf ("Max UNTIL year: %i\n", data.max_until_year); #endif *max_until_year = data.max_until_year; } /* Converts the line into fields. */ static void parse_fields (ParsingData *data) { int i; char *p, *s, ch; /* Reset all fields to NULL. */ for (i = 0; i < MAX_FIELDS; i++) data->fields[i] = 0; data->num_fields = 0; p = data->buffer; for (;;) { /* Skip whitespace. */ while (isspace (*p)) p++; /* See if we have reached the end of the line or a comment. */ if (*p == '\0' || *p == '#') break; /* We must have another field, so save the start position. */ data->fields[data->num_fields++] = p; /* Now find the end of the field. If the field contains '"' characters they are removed and we have to move the rest of the chars back. */ s = p; for (;;) { ch = *p; if (ch == '\0' || ch == '#') { /* Don't move p on since this is the end of the line. */ *s = '\0'; break; } else if (isspace (ch)) { *s = '\0'; p++; break; } else if (ch == '"') { p++; for (;;) { ch = *p; if (ch == '\0') { fprintf (stderr, "%s:%i: Closing quote character ('\"') missing.\n%s\n", data->filename, data->line_number, data->line); exit (1); } else if (ch == '"') { p++; break; } else { *s++ = ch; } p++; } } else { *s++ = ch; } p++; } } #if 0 printf ("%i fields: ", data->num_fields); for (i = 0; i < data->num_fields; i++) printf ("'%s' ", data->fields[i]); printf ("\n"); #endif } static gboolean parse_zone_line (ParsingData *data) { ZoneData zone; /* All 5 fields up to FORMAT must be present. */ if (data->num_fields < 5 || data->num_fields > 9) { fprintf (stderr, "%s:%i: Invalid Zone line - %i fields.\n%s\n", data->filename, data->line_number, data->num_fields, data->line); exit (1); } zone.zone_name = g_strdup (data->fields[ZONE_NAME]); zone.zone_line_data = g_array_new (FALSE, FALSE, sizeof (ZoneLineData)); g_array_append_val (data->zone_data, zone); return parse_zone_common (data, 0); } static gboolean parse_zone_continuation_line (ParsingData *data) { /* All 3 fields up to FORMAT must be present. */ if (data->num_fields < 3 || data->num_fields > 7) { fprintf (stderr, "%s:%i: Invalid Zone continuation line - %i fields.\n%s\n", data->filename, data->line_number, data->num_fields, data->line); exit (1); } return parse_zone_common (data, -2); } static gboolean parse_zone_common (ParsingData *data, int offset) { ZoneData *zone; ZoneLineData zone_line; TimeCode time_code; zone_line.stdoff_seconds = parse_time (data, data->fields[ZONE_GMTOFF + offset], &time_code); zone_line.save_seconds = parse_rules_save (data, data->fields[ZONE_RULES_SAVE + offset], &zone_line.rules); if (!VzicPureOutput) { /* We round the UTC offsets to the nearest minute, to be compatible with Outlook. This also works with -ve numbers, I think. -56 % 60 = -59. -61 % 60 = -1. */ if (zone_line.stdoff_seconds >= 0) zone_line.stdoff_seconds += 30; else zone_line.stdoff_seconds -= 29; zone_line.stdoff_seconds -= zone_line.stdoff_seconds % 60; if (zone_line.save_seconds >= 0) zone_line.save_seconds += 30; else zone_line.save_seconds -= 29; zone_line.save_seconds -= zone_line.save_seconds % 60; } zone_line.format = g_strdup (data->fields[ZONE_FORMAT + offset]); if (data->num_fields - offset >= 6) { zone_line.until_set = TRUE; zone_line.until_year = parse_year (data, data->fields[ZONE_UNTIL_YEAR + offset], FALSE, 0); zone_line.until_month = parse_month (data, data->fields[ZONE_UNTIL_MONTH + offset]); zone_line.until_day_code = parse_day (data, data->fields[ZONE_UNTIL_DAY + offset], &zone_line.until_day_number, &zone_line.until_day_weekday); zone_line.until_time_seconds = parse_time (data, data->fields[ZONE_UNTIL_TIME + offset], &zone_line.until_time_code); /* We also want to know the maximum year used in any UNTIL value, so we know where to expand all the infinite Rule data to. */ if (zone_line.until_year != YEAR_MAXIMUM && zone_line.until_year != YEAR_MINIMUM) data->max_until_year = MAX (data->max_until_year, zone_line.until_year); } else { zone_line.until_set = FALSE; } /* Append it to the last Zone, since that is the one we are currently reading. */ zone = &g_array_index (data->zone_data, ZoneData, data->zone_data->len - 1); g_array_append_val (zone->zone_line_data, zone_line); return zone_line.until_set; } static void parse_rule_line (ParsingData *data) { GArray *rule_array; RuleData rule; char *name; TimeCode time_code; /* All 10 fields must be present. */ if (data->num_fields != 10) { fprintf (stderr, "%s:%i: Invalid Rule line - %i fields.\n%s\n", data->filename, data->line_number, data->num_fields, data->line); exit (1); } name = data->fields[RULE_NAME]; /* Create the GArray and add it to the hash table if it doesn't already exist. */ rule_array = g_hash_table_lookup (data->rule_data, name); if (!rule_array) { rule_array = g_array_new (FALSE, FALSE, sizeof (RuleData)); g_hash_table_insert (data->rule_data, g_strdup (name), rule_array); } rule.from_year = parse_year (data, data->fields[RULE_FROM], FALSE, 0); if (rule.from_year == YEAR_MAXIMUM) { fprintf (stderr, "%s:%i: Invalid Rule FROM value: '%s'\n", data->filename, data->line_number, data->fields[RULE_FROM]); exit (1); } rule.to_year = parse_year (data, data->fields[RULE_TO], TRUE, rule.from_year); if (rule.to_year == YEAR_MINIMUM) { fprintf (stderr, "%s:%i: Invalid Rule TO value: %s\n", data->filename, data->line_number, data->fields[RULE_TO]); exit (1); } if (!strcmp (data->fields[RULE_TYPE], "-")) rule.type = NULL; else { printf ("Type: %s\n", data->fields[RULE_TYPE]); rule.type = g_strdup (data->fields[RULE_TYPE]); } rule.in_month = parse_month (data, data->fields[RULE_IN]); rule.on_day_code = parse_day (data, data->fields[RULE_ON], &rule.on_day_number, &rule.on_day_weekday); rule.at_time_seconds = parse_time (data, data->fields[RULE_AT], &rule.at_time_code); rule.save_seconds = parse_time (data, data->fields[RULE_SAVE], &time_code); if (!strcmp (data->fields[RULE_LETTER_S], "-")) { rule.letter_s = NULL; } else { rule.letter_s = g_strdup (data->fields[RULE_LETTER_S]); } rule.is_shallow_copy = FALSE; g_array_append_val (rule_array, rule); } static void parse_link_line (ParsingData *data) { char *from, *to, *old_from; GList *zone_list; /* We must have 3 fields for a Link. */ if (data->num_fields != 3) { fprintf (stderr, "%s:%i: Invalid Rule line - %i fields.\n%s\n", data->filename, data->line_number, data->num_fields, data->line); exit (1); } from = data->fields[LINK_FROM]; to = data->fields[LINK_TO]; #if 0 printf ("LINK FROM: %s\tTO: %s\n", from, to); #endif if (g_hash_table_lookup_extended (data->link_data, from, (gpointer) &old_from, (gpointer) &zone_list)) { from = old_from; } else { from = g_strdup (from); zone_list = NULL; } zone_list = g_list_prepend (zone_list, g_strdup (to)); g_hash_table_insert (data->link_data, from, zone_list); } static int parse_year (ParsingData *data, char *field, gboolean accept_only, int only_value) { int len, year = 0; char *p; if (!field) { fprintf (stderr, "%s:%i: Missing year.\n%s\n", data->filename, data->line_number, data->line); exit (1); } len = strlen (field); if (accept_only && !strncmp (field, "only", len)) return only_value; if (len >= 2) { if (!strncmp (field, "maximum", len)) return YEAR_MAXIMUM; else if (!strncmp (field, "minimum", len)) return YEAR_MINIMUM; } for (p = field; *p; p++) { if (*p < '0' || *p > '9') { fprintf (stderr, "%s:%i: Invalid year: %s\n%s\n", data->filename, data->line_number, field, data->line); exit (1); } year = year * 10 + *p - '0'; } if (year < 1000 || year > 2037) { fprintf (stderr, "%s:%i: Strange year: %s\n%s\n", data->filename, data->line_number, field, data->line); exit (1); } return year; } /* Parses a month name, returning 0 (Jan) to 11 (Dec). */ static int parse_month (ParsingData *data, char *field) { static char* months[] = { "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december" }; char *p; int len, i; /* If the field is missing, it must be the optional UNTIL month, so we return 0 for January. */ if (!field) return 0; for (p = field, len = 0; *p; p++, len++) { *p = tolower (*p); } for (i = 0; i < 12; i++) { if (!strncmp (field, months[i], len)) return i; } fprintf (stderr, "%s:%i: Invalid month: %s\n%s\n", data->filename, data->line_number, field, data->line); exit (1); } /* Parses a day specifier, returning a code representing the type of match together with a day of the month and a weekday number (0=Sun). */ static DayCode parse_day (ParsingData *data, char *field, int *day, int *weekday) { char *day_part, *p; DayCode day_code; if (!field) { *day = 1; return DAY_SIMPLE; } *day = *weekday = 0; if (!strncmp (field, "last", 4)) { *weekday = parse_weekday (data, field + 4); /* We set the day to the end of the month to make sorting Rules easy. */ *day = 31; return DAY_LAST_WEEKDAY; } day_part = field; day_code = DAY_SIMPLE; for (p = field; *p; p++) { if (*p == '<' || *p == '>') { if (*(p + 1) == '=') { day_code = (*p == '<') ? DAY_WEEKDAY_ON_OR_BEFORE : DAY_WEEKDAY_ON_OR_AFTER; *p = '\0'; *weekday = parse_weekday (data, field); day_part = p + 2; break; } fprintf (stderr, "%s:%i: Invalid day: %s\n%s\n", data->filename, data->line_number, field, data->line); exit (1); } } for (p = day_part; *p; p++) { if (*p < '0' || *p > '9') { fprintf (stderr, "%s:%i: Invalid day: %s\n%s\n", data->filename, data->line_number, field, data->line); exit (1); } *day = *day * 10 + *p - '0'; } if (*day < 1 || *day > 31) { fprintf (stderr, "%s:%i: Invalid day: %s\n%s\n", data->filename, data->line_number, field, data->line); exit (1); } return day_code; } /* Parses a weekday name, returning 0 (Sun) to 6 (Sat). */ static int parse_weekday (ParsingData *data, char *field) { static char* weekdays[] = { "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" }; char *p; int len, i; for (p = field, len = 0; *p; p++, len++) { *p = tolower (*p); } for (i = 0; i < 7; i++) { if (!strncmp (field, weekdays[i], len)) return i; } fprintf (stderr, "%s:%i: Invalid weekday: %s\n%s\n", data->filename, data->line_number, field, data->line); exit (1); } /* Parses a time (hour + minute + second) and returns the result in seconds, together with a time code specifying whether it is Wall clock time, local standard time, or universal time. The time can start with a '-' in which case it will be negative. */ static int parse_time (ParsingData *data, char *field, TimeCode *time_code) { char *p; int hours = 0, minutes = 0, seconds = 0, result, negative = 0; if (!field) { *time_code = TIME_WALL; return 0; } p = field; if (*p == '-') { p++; negative = 1; } hours = parse_number (data, &p); if (*p == ':') { p++; minutes = parse_number (data, &p); if (*p == ':') { p++; seconds = parse_number (data, &p); } } if (hours < 0 || hours > 24 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59 || (hours == 24 && (minutes != 0 || seconds != 0))) { fprintf (stderr, "%s:%i: Invalid time: %s\n%s\n", data->filename, data->line_number, field, data->line); exit (1); } if (hours == 24) { hours = 23; minutes = 59; seconds = 59; } #if 0 printf ("Time: %s -> %i:%02i:%02i\n", field, hours, minutes, seconds); #endif result = hours * 3600 + minutes * 60 + seconds; if (negative) result = -result; if (*p == '\0') { *time_code = TIME_WALL; return result; } if (*(p + 1) == '\0') { if (*p == 'w') { *time_code = TIME_WALL; return result; } else if (*p == 's') { *time_code = TIME_STANDARD; return result; } else if (*p == 'u' || *p == 'g' || *p == 'z') { *time_code = TIME_UNIVERSAL; return result; } } fprintf (stderr, "%s:%i: Invalid time: %s\n%s\n", data->filename, data->line_number, field, data->line); exit (1); } /* Parses a simple number and returns the result. The pointer argument is moved to the first character after the number. */ static int parse_number (ParsingData *data, char **num) { char *p; int result; p = *num; #if 0 printf ("In parse_number p:%s\n", p); #endif if (*p < '0' || *p > '9') { fprintf (stderr, "%s:%i: Invalid number: %s\n%s\n", data->filename, data->line_number, *num, data->line); exit (1); } result = *p++ - '0'; while (*p >= '0' && *p <= '9') result = result * 10 + *p++ - '0'; *num = p; return result; } static int parse_rules_save (ParsingData *data, char *field, char **rules) { TimeCode time_code; *rules = NULL; /* Check for just "-". */ if (field[0] == '-' && field[1] == '\0') return 0; /* Check for a time to add to local standard time. We don't care about a time code here, since it is just an offset. */ if (*field == '-' || (*field >= '0' && *field <= '9')) return parse_time (data, field, &time_code); /* It must be a rules name. */ *rules = g_strdup (field); return 0; } GHashTable* parse_zone_tab (char *filename) { GHashTable *zones_hash; ZoneDescription *zone_desc; FILE *fp; char buf[4096]; gchar **fields, *zone_name, *latitude, *longitude, *p; fp = fopen (filename, "r"); if (!fp) { fprintf (stderr, "Couldn't open file: %s\n", filename); exit (1); } zones_hash = g_hash_table_new (g_str_hash, g_str_equal); while (fgets (buf, sizeof(buf), fp)) { if (*buf == '#') continue; g_strchomp (buf); fields = g_strsplit (buf,"\t", 4); if (strlen (fields[0]) != 2) { fprintf (stderr, "Invalid zone description line: %s\n", buf); exit (1); } zone_name = g_strdup (fields[2]); zone_desc = g_new (ZoneDescription, 1); zone_desc->country_code[0] = fields[0][0]; zone_desc->country_code[1] = fields[0][1]; zone_desc->comment = (fields[3] && fields[3][0]) ? g_strdup (fields[3]) : NULL; /* Now parse the latitude and longitude. */ latitude = fields[1]; longitude = latitude + 1; while (*longitude != '+' && *longitude != '-') longitude++; parse_coord (latitude, longitude - latitude, zone_desc->latitude); parse_coord (longitude, strlen (longitude), zone_desc->longitude); g_hash_table_insert (zones_hash, zone_name, zone_desc); #if 0 g_print ("Found zone: %s %i %02i %02i,%i %02i %02i\n", zone_name, zone_desc->latitude[0], zone_desc->latitude[1], zone_desc->latitude[2], zone_desc->longitude[0], zone_desc->longitude[1], zone_desc->longitude[2]); #endif } fclose (fp); return zones_hash; } static void parse_coord (char *coord, int len, int *result) { int degrees = 0, minutes = 0, seconds = 0; if (len == 5) sscanf (coord + 1, "%2d%2d", °rees, &minutes); else if (len == 6) sscanf (coord + 1, "%3d%2d", °rees, &minutes); else if (len == 7) sscanf (coord + 1, "%2d%2d%2d", °rees, &minutes, &seconds); else if (len == 8) sscanf (coord + 1, "%3d%2d%2d", °rees, &minutes, &seconds); else { fprintf (stderr, "Invalid coordinate: %s\n", coord); exit (1); } if (coord[0] == '-') degrees = -degrees; result[0] = degrees; result[1] = minutes; result[2] = seconds; }