diff options
Diffstat (limited to 'src/ical.c')
-rw-r--r-- | src/ical.c | 1229 |
1 files changed, 796 insertions, 433 deletions
@@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -36,11 +36,14 @@ #include <strings.h> #include <sys/types.h> +#include <ctype.h> #include "calcurse.h" #define ICALDATEFMT "%Y%m%d" #define ICALDATETIMEFMT "%Y%m%dT%H%M%S" +#define SEPARATOR "-- \n" +#define INDENT " " typedef enum { ICAL_VEVENT, @@ -59,17 +62,9 @@ typedef enum { SUMMARY, DESCRIPTION, LOCATION, - COMMENT, - STATUS + COMMENT } ical_property_e; -typedef struct { - enum recur_type type; - int freq; - long until; - unsigned count; -} ical_rpt_t; - static void ical_export_header(FILE *); static void ical_export_recur_events(FILE *, int); static void ical_export_events(FILE *, int); @@ -81,22 +76,29 @@ static void ical_export_footer(FILE *); static const char *ical_recur_type[NBRECUR] = { "DAILY", "WEEKLY", "MONTHLY", "YEARLY" }; -/* Escape characters in field before printing */ -static void ical_format_line(FILE * stream, char * field, char * msg) +static const char *ical_wday[] = + {"SU", "MO", "TU", "WE", "TH", "FR", "SA"}; + +/* + * Encode a string as a property value of type TEXT (RFC 5545, 3.3.11). + */ +static void ical_format_line(FILE *stream, char *property, char *msg) { char * p; - fputs(field, stream); + fputs(property, stream); for (p = msg; *p; p++) { switch (*p) { + case '\n': + fprintf(stream, "\\%c", 'n'); + break; case ',': case ';': case '\\': fprintf(stream, "\\%c", *p); break; default: - fputc(*p, stream); - break; + fputc(*p, stream); } } fputc('\n', stream); @@ -113,12 +115,156 @@ static void ical_export_valarm(FILE * stream) fputs("END:VALARM\n", stream); } +static void ical_export_rrule(FILE *stream, struct rpt *rpt, ical_vevent_e item, + char *buf) +{ + llist_item_t *j; + int d; + char *fmt = item == EVENT ? ICALDATEFMT : + item == APPOINTMENT ? ICALDATETIMEFMT : + NULL; + + fprintf(stream, "RRULE:FREQ=%s", ical_recur_type[rpt->type]); + if (rpt->freq > 1) + fprintf(stream, ";INTERVAL=%d", rpt->freq); + if (rpt->until) { + date_sec2date_fmt(rpt->until, fmt, buf); + fprintf(stream, ";UNTIL=%s", buf); + } + if (LLIST_FIRST(&rpt->bymonth)) { + fputs(";BYMONTH=", stream); + LLIST_FOREACH(&rpt->bymonth, j) { + d = *(int *)LLIST_GET_DATA(j); + fprintf(stream, "%d", d); + if (LLIST_NEXT(j)) + fputc(',', stream); + } + } + if (LLIST_FIRST(&rpt->bywday)) { + int ord; + char sign; + + fputs(";BYDAY=", stream); + LLIST_FOREACH(&rpt->bywday, j) { + d = *(int *)LLIST_GET_DATA(j); + sign = d < 0 ? '-' : '+'; + d = abs(d); + ord = d / 7; + d = d % 7; + if (ord == 0) + fprintf(stream, "%s", ical_wday[d]); + else + fprintf(stream, "%c%d%s", sign, ord, ical_wday[d]); + if (LLIST_NEXT(j)) + fputc(',', stream); + } + } + if (LLIST_FIRST(&rpt->bymonthday)) { + fputs(";BYMONTHDAY=", stream); + LLIST_FOREACH(&rpt->bymonthday, j) { + d = *(int *)LLIST_GET_DATA(j); + fprintf(stream, "%d", d); + if (LLIST_NEXT(j)) + fputc(',', stream); + } + } + fputc('\n', stream); +} + +/* + * Copy the characters (lines) between "a" and "z" into an allocated string, + * un-indent it and return it. Note that the final character, a newline, is + * overwritten with '\0'. + */ +static char *ical_unindent(char *a, char *z) +{ + char *p, *q; int len; + + len = z - a + 1; + + p = mem_malloc(len); + strncpy(p, a, len); + p[len - 1] = '\0'; + while ((q = strstr(p, "\n" INDENT))) { + while (*(q + 1 + strlen(INDENT))) { + *(q + 1) = *(q + 1 + strlen(INDENT)); + q++; + } + *(q + 1) = '\0'; + } + return p; +} + +static void ical_export_note(FILE *stream, char *name) +{ + char *note_file, *p, *q, *r, *rest; + char *property[] = { + "Location: ", + "Comment: ", + NULL + }; + char *PROPERTY[] = { + "LOCATION:", + "COMMENT:" + }; + struct string note; + char lbuf[BUFSIZ]; + FILE *fp; + int has_desc, has_prop, i; + + asprintf(¬e_file, "%s/%s", path_notes, name); + if (!(fp = fopen(note_file, "r")) || ungetc(getc(fp), fp) == EOF) { + fclose(fp); + return; + } + string_init(¬e); + while (fgets(lbuf, BUFSIZ, fp)) + string_catf(¬e, "%s", lbuf); + fclose(fp); + + has_desc = has_prop = 0; + rest = note.buf; + if ((p = strstr(note.buf, SEPARATOR))) { + has_prop = 1; + rest = p + strlen(SEPARATOR); + if (p != note.buf) { + has_desc = 1; + *(--p) = '\0'; + } + } else { + has_desc = 1; + note.buf[strlen(note.buf) - 1] = '\0'; + } + + if (has_desc) + ical_format_line(stream, "DESCRIPTION:", note.buf); + + if (!has_prop) + goto cleanup; + for (i = 0; property[i]; i++) { + if ((p = strstr(rest, property[i]))) + p += strlen(property[i]); + else + continue; + /* Find end of property. */ + for (q = p; + (q = strchr(q, '\n')) && starts_with(q + 1, INDENT); + q++) ; + /* Extract property line(s). */ + r = ical_unindent(p, q); + ical_format_line(stream, PROPERTY[i], r); + mem_free(r); + } +cleanup: + mem_free(note.buf); +} + /* Export header. */ static void ical_export_header(FILE * stream) { fputs("BEGIN:VCALENDAR\n", stream); - fprintf(stream, "PRODID:-//calcurse//NONSGML v%s//EN\n", VERSION); fputs("VERSION:2.0\n", stream); + fprintf(stream, "PRODID:-//calcurse//NONSGML v%s//EN\n", VERSION); } /* Export footer. */ @@ -131,26 +277,21 @@ static void ical_export_footer(FILE * stream) static void ical_export_recur_events(FILE * stream, int export_uid) { llist_item_t *i, *j; - char ical_date[BUFSIZ]; + char ical_date[BUFSIZ], *hash; LLIST_FOREACH(&recur_elist, i) { struct recur_event *rev = LLIST_GET_DATA(i); - date_sec2date_fmt(rev->day, ICALDATEFMT, ical_date); fputs("BEGIN:VEVENT\n", stream); - fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date); - fprintf(stream, "RRULE:FREQ=%s;INTERVAL=%d", - ical_recur_type[rev->rpt->type], rev->rpt->freq); - - if (rev->rpt->until != 0) { - date_sec2date_fmt(rev->rpt->until, ICALDATEFMT, - ical_date); - fprintf(stream, ";UNTIL=%s\n", ical_date); - } else { - fputc('\n', stream); + if (export_uid) { + hash = recur_event_hash(rev); + fprintf(stream, "UID:%s\n", hash); + mem_free(hash); } - + date_sec2date_fmt(rev->day, ICALDATEFMT, ical_date); + fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date); + ical_export_rrule(stream, rev->rpt, EVENT, ical_date); if (LLIST_FIRST(&rev->exc)) { - fputs("EXDATE:", stream); + fputs("EXDATE;VALUE=DATE:", stream); LLIST_FOREACH(&rev->exc, j) { struct excp *exc = LLIST_GET_DATA(j); date_sec2date_fmt(exc->st, ICALDATETIMEFMT, @@ -159,15 +300,9 @@ static void ical_export_recur_events(FILE * stream, int export_uid) fputc(LLIST_NEXT(j) ? ',' : '\n', stream); } } - ical_format_line(stream, "SUMMARY:", rev->mesg); - - if (export_uid) { - char *hash = recur_event_hash(rev); - fprintf(stream, "UID:%s\n", hash); - mem_free(hash); - } - + if (rev->note) + ical_export_note(stream, rev->note); fputs("END:VEVENT\n", stream); } } @@ -176,21 +311,21 @@ static void ical_export_recur_events(FILE * stream, int export_uid) static void ical_export_events(FILE * stream, int export_uid) { llist_item_t *i; - char ical_date[BUFSIZ]; + char ical_date[BUFSIZ], *hash; LLIST_FOREACH(&eventlist, i) { struct event *ev = LLIST_TS_GET_DATA(i); - date_sec2date_fmt(ev->day, ICALDATEFMT, ical_date); fputs("BEGIN:VEVENT\n", stream); - fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date); - ical_format_line(stream, "SUMMARY:", ev->mesg); - if (export_uid) { - char *hash = event_hash(ev); + hash = event_hash(ev); fprintf(stream, "UID:%s\n", hash); mem_free(hash); } - + date_sec2date_fmt(ev->day, ICALDATEFMT, ical_date); + fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date); + ical_format_line(stream, "SUMMARY:", ev->mesg); + if (ev->note) + ical_export_note(stream, ev->note); fputs("END:VEVENT\n", stream); } } @@ -199,16 +334,30 @@ static void ical_export_events(FILE * stream, int export_uid) static void ical_export_recur_apoints(FILE * stream, int export_uid) { llist_item_t *i, *j; - char ical_datetime[BUFSIZ]; - char ical_date[BUFSIZ]; + char ical_datetime[BUFSIZ], *hash; + time_t tod; LLIST_TS_LOCK(&recur_alist_p); LLIST_TS_FOREACH(&recur_alist_p, i) { struct recur_apoint *rapt = LLIST_TS_GET_DATA(i); + /* + * Add time-of-day to UNTIL/EXDATE. + * In calcurse until/exception is a date (midnight), but in + * RFC 5545 UNTIL/EXDATE is a DATE-TIME value type by default. + */ + tod = get_item_time(rapt->start); + if (rapt->rpt->until) + rapt->rpt->until += tod; + date_sec2date_fmt(rapt->start, ICALDATETIMEFMT, ical_datetime); fputs("BEGIN:VEVENT\n", stream); + if (export_uid) { + hash = recur_apoint_hash(rapt); + fprintf(stream, "UID:%s\n", hash); + mem_free(hash); + } fprintf(stream, "DTSTART:%s\n", ical_datetime); if (rapt->dur > 0) { fprintf(stream, "DURATION:P%ldDT%ldH%ldM%ldS\n", @@ -217,38 +366,22 @@ static void ical_export_recur_apoints(FILE * stream, int export_uid) (rapt->dur / MININSEC) % HOURINMIN, rapt->dur % MININSEC); } - fprintf(stream, "RRULE:FREQ=%s;INTERVAL=%d", - ical_recur_type[rapt->rpt->type], rapt->rpt->freq); - - if (rapt->rpt->until != 0) { - date_sec2date_fmt(rapt->rpt->until + HOURINSEC, - ICALDATEFMT, ical_date); - fprintf(stream, ";UNTIL=%s\n", ical_date); - } else { - fputc('\n', stream); - } - + ical_export_rrule(stream, rapt->rpt, APPOINTMENT, ical_datetime); if (LLIST_FIRST(&rapt->exc)) { fputs("EXDATE:", stream); LLIST_FOREACH(&rapt->exc, j) { struct excp *exc = LLIST_GET_DATA(j); - date_sec2date_fmt(exc->st, ICALDATETIMEFMT, - ical_date); - fprintf(stream, "%s", ical_date); + date_sec2date_fmt(exc->st + tod, ICALDATETIMEFMT, + ical_datetime); + fprintf(stream, "%s", ical_datetime); fputc(LLIST_NEXT(j) ? ',' : '\n', stream); } } - ical_format_line(stream, "SUMMARY:", rapt->mesg); + if (rapt->note) + ical_export_note(stream, rapt->note); if (rapt->state & APOINT_NOTIFY) ical_export_valarm(stream); - - if (export_uid) { - char *hash = recur_apoint_hash(rapt); - fprintf(stream, "UID:%s\n", hash); - mem_free(hash); - } - fputs("END:VEVENT\n", stream); } LLIST_TS_UNLOCK(&recur_alist_p); @@ -258,14 +391,19 @@ static void ical_export_recur_apoints(FILE * stream, int export_uid) static void ical_export_apoints(FILE * stream, int export_uid) { llist_item_t *i; - char ical_datetime[BUFSIZ]; + char ical_datetime[BUFSIZ], *hash; LLIST_TS_LOCK(&alist_p); LLIST_TS_FOREACH(&alist_p, i) { struct apoint *apt = LLIST_TS_GET_DATA(i); + fputs("BEGIN:VEVENT\n", stream); + if (export_uid) { + hash = apoint_hash(apt); + fprintf(stream, "UID:%s\n", hash); + mem_free(hash); + } date_sec2date_fmt(apt->start, ICALDATETIMEFMT, ical_datetime); - fputs("BEGIN:VEVENT\n", stream); fprintf(stream, "DTSTART:%s\n", ical_datetime); if (apt->dur > 0) { fprintf(stream, "DURATION:P%ldDT%ldH%ldM%ldS\n", @@ -275,15 +413,10 @@ static void ical_export_apoints(FILE * stream, int export_uid) apt->dur % MININSEC); } ical_format_line(stream, "SUMMARY:", apt->mesg); + if (apt->note) + ical_export_note(stream, apt->note); if (apt->state & APOINT_NOTIFY) ical_export_valarm(stream); - - if (export_uid) { - char *hash = apoint_hash(apt); - fprintf(stream, "UID:%s\n", hash); - mem_free(hash); - } - fputs("END:VEVENT\n", stream); } LLIST_TS_UNLOCK(&alist_p); @@ -293,22 +426,23 @@ static void ical_export_apoints(FILE * stream, int export_uid) static void ical_export_todo(FILE * stream, int export_uid) { llist_item_t *i; + char *hash; LLIST_FOREACH(&todolist, i) { struct todo *todo = LLIST_TS_GET_DATA(i); fputs("BEGIN:VTODO\n", stream); - if (todo->completed) - fprintf(stream, "STATUS:COMPLETED\n"); - fprintf(stream, "PRIORITY:%d\n", todo->id); - ical_format_line(stream, "SUMMARY:", todo->mesg); - if (export_uid) { - char *hash = todo_hash(todo); + hash = todo_hash(todo); fprintf(stream, "UID:%s\n", hash); mem_free(hash); } - + fprintf(stream, "PRIORITY:%d\n", todo->id); + ical_format_line(stream, "SUMMARY:", todo->mesg); + if (todo->note) + ical_export_note(stream, todo->note); + if (todo->completed) + fprintf(stream, "STATUS:COMPLETED\n"); fputs("END:VTODO\n", stream); } } @@ -373,32 +507,37 @@ static void ical_store_todo(int priority, int completed, char *mesg, erase_note(¬e); } +/* + * Calcurse limitation: events are one-day (all-day), and all multi-day events + * are turned into one-day events; a note has been added by ical_read_event(). + */ static void -ical_store_event(char *mesg, char *note, long day, long end, - ical_rpt_t *irpt, llist_t *exc, const char *fmt_ev, +ical_store_event(char *mesg, char *note, time_t day, time_t end, + struct rpt *rpt, llist_t *exc, const char *fmt_ev, const char *fmt_rev) { const int EVENTID = 1; struct event *ev; struct recur_event *rev; - if (irpt) { - struct rpt rpt; - rpt.type = irpt->type; - rpt.freq = irpt->freq; - rpt.until = irpt->until; - LLIST_INIT(&rpt.bymonth); - LLIST_INIT(&rpt.bywday); - LLIST_INIT(&rpt.bymonthday); - rpt.exc = *exc; - rev = recur_event_new(mesg, note, day, EVENTID, &rpt); - mem_free(irpt); + if (!mesg) + mesg = mem_strdup(_("(empty)")); + EXIT_IF(!mesg, _("ical_store_event: out of memory")); + + /* + * Repeating event. The end day is ignored, and the event becomes + * one-day even if multi-day. + */ + if (rpt) { + rpt->exc = *exc; + rev = recur_event_new(mesg, note, day, EVENTID, rpt); if (fmt_rev) print_recur_event(fmt_rev, day, rev); goto cleanup; } - if (end == 0 || end - day <= DAYINSEC) { + /* Ordinary one-day event. */ + if (end - day <= DAYINSEC) { ev = event_new(mesg, note, day, EVENTID); if (fmt_ev) print_event(fmt_ev, day, ev); @@ -406,22 +545,19 @@ ical_store_event(char *mesg, char *note, long day, long end, } /* - * Here we have an event that spans over several days. - * - * In iCal, the end specifies when the event is supposed to end, in - * calcurse, the end specifies the time that the last occurrence of the - * event starts, so we need to do some conversion here. + * Ordinary multi-day event. The event is turned into a daily repeating + * event until the day before the end. In iCal, the end day is + * exclusive, the until day inclusive. */ - end = day + ((end - day - 1) / DAYINSEC) * DAYINSEC; - struct rpt rpt; - rpt.type = RECUR_DAILY; - rpt.freq = 1; - rpt.until = end; - LLIST_INIT(&rpt.bymonth); - LLIST_INIT(&rpt.bywday); - LLIST_INIT(&rpt.bymonthday); - rpt.exc = *exc; - rev = recur_event_new(mesg, note, day, EVENTID, &rpt); + struct rpt tmp; + tmp.type = RECUR_DAILY; + tmp.freq = 1; + tmp.until = day + ((end - day - 1) / DAYINSEC) * DAYINSEC; + LLIST_INIT(&tmp.bymonth); + LLIST_INIT(&tmp.bywday); + LLIST_INIT(&tmp.bymonthday); + tmp.exc = *exc; + rev = recur_event_new(mesg, note, day, EVENTID, &tmp); if (fmt_rev) print_recur_event(fmt_rev, day, rev); @@ -431,27 +567,41 @@ cleanup: } static void -ical_store_apoint(char *mesg, char *note, long start, long dur, - ical_rpt_t * irpt, llist_t * exc, int has_alarm, +ical_store_apoint(char *mesg, char *note, time_t start, long dur, + struct rpt *rpt, llist_t *exc, int has_alarm, const char *fmt_apt, const char *fmt_rapt) { char state = 0L; struct apoint *apt; struct recur_apoint *rapt; + time_t day; + + if (!mesg) + mesg = mem_strdup(_("(empty)")); + EXIT_IF(!mesg, _("ical_store_event: out of memory")); if (has_alarm) state |= APOINT_NOTIFY; - if (irpt) { - struct rpt rpt; - rpt.type = irpt->type; - rpt.freq = irpt->freq; - rpt.until = irpt->until; - LLIST_INIT(&rpt.bymonth); - LLIST_INIT(&rpt.bywday); - LLIST_INIT(&rpt.bymonthday); - rpt.exc = *exc; - rapt = recur_apoint_new(mesg, note, start, dur, state, &rpt); - mem_free(irpt); + if (rpt) { + /* + * In calcurse, "until" is interpreted as a day (DATE) - hours, + * minutes and seconds are ignored - whereas in iCal the full + * DATE-TIME value of "until" is taken into account. It follows + * that if the event in calcurse has an occurrence on the until + * day, and the start time is after the until value, the + * calcurse until day must be changed to the day before. + */ + if (rpt->until) { + day = DAY(rpt->until); + if (recur_item_find_occurrence(start, dur, rpt, NULL, + day, NULL) && + get_item_time(rpt->until) < get_item_time(start)) + rpt->until = date_sec_change(day, 0, -1); + else + rpt->until = day; + } + rpt->exc = *exc; + rapt = recur_apoint_new(mesg, note, start, dur, state, rpt); if (fmt_rapt) print_recur_apoint(fmt_rapt, start, rapt->start, rapt); } else { @@ -473,7 +623,6 @@ static char *ical_unformat_line(char *line, int eol, int indentation) { struct string s; char *p; - const char *INDENT = " "; string_init(&s); for (p = line; *p; p++) { @@ -579,6 +728,32 @@ ical_chk_header(FILE * fd, char *buf, char *lstore, unsigned *lineno, } /* + * Return the TZID property parameter value from a DTSTART/DTEND/EXDATE property + * in an allocated string. The value may be any text string not containing the + * characters '"', ';', ':' and ',' (RFC 5545, sections 3.2.19 and 3.1). + */ +static char *ical_get_tzid(char *p) +{ + const char param[] = ";TZID="; + char *q; + int s; + + if (!(p = strstr(p, param))) + return NULL; + p += sizeof(param) - 1; + if (*p == '"') + return NULL; + + q = strpbrk(p, ":;"); + s = q - p + 1; + q = mem_malloc(s); + strncpy(q, p, s); + q[s - 1] = '\0'; + + return q; +} + +/* * Return event type from a DTSTART/DTEND/EXDATE property. */ static ical_vevent_e ical_get_type(char *c_line) @@ -601,23 +776,23 @@ static ical_vevent_e ical_get_type(char *c_line) * where DATE is 'YYYYMMDD' and TIME is 'HHMMSS'. * The time and 'T' separator are optional (in the case of an day-long event). * - * The type argument is either APPOINTMENT or EVENT and the time format must - * agree. - * - * The timezone is not yet handled by calcurse. + * The type argument is either APPOINTMENT or EVENT, and the time format must + * match (either DATE-TIME or DATE). The time zone identifier is ignored in an + * EVENT or in an APPOINTMENT with UTC time. */ -static time_t ical_datetime2time_t(char *datestr, ical_vevent_e type) +static time_t ical_datetime2time_t(char *datestr, char *tzid, ical_vevent_e type) { const int INVALID = 0, DATE = 3, DATETIME = 6, DATETIMEZ = 7; struct date date; unsigned hour, min, sec; - char c; + char c, UTC[] = ""; int format; EXIT_IF(type == UNDEFINED, "event type not set"); format = sscanf(datestr, "%04u%02u%02uT%02u%02u%02u%c", &date.yyyy, &date.mm, &date.dd, &hour, &min, &sec, &c); + if (format == DATE && strlen(datestr) > 8) format = INVALID; if (format == DATETIMEZ && c != 'Z') @@ -626,9 +801,9 @@ static time_t ical_datetime2time_t(char *datestr, ical_vevent_e type) if (format == DATE && type == EVENT) return date2sec(date, 0, 0); else if (format == DATETIME && type == APPOINTMENT) - return date2sec(date, hour, min); + return tzdate2sec(date, hour, min, tzid); else if (format == DATETIMEZ && type == APPOINTMENT) - return utcdate2sec(date, hour, min); + return tzdate2sec(date, hour, min, UTC); return 0; } @@ -689,6 +864,7 @@ static long ical_durtime2long(char *timestr) * dur-second = 1*DIGIT "S" * dur-day = 1*DIGIT "D" * + * For events, duration must be days or weeks. * Example: A duration of 15 days, 5 hours and 20 seconds would be: * P15DT5H0M20S * A duration of 7 weeks would be: @@ -717,42 +893,15 @@ static long ical_dur2long(char *durstr, ical_vevent_e type) else if (sscanf(p, "%u%c%n", &day, &c, &bytes_read) == 2 && c == 'D') { /* dur-date */ p += bytes_read; - if (*p == 'T' && type == APPOINTMENT) - return day * DAYINSEC + ical_durtime2long(p); - else if (*p != 'T' && type == EVENT) - return day * DAYINSEC; + return day * DAYINSEC + (*p == 'T' && type == APPOINTMENT ? + ical_durtime2long(p) : + 0); } return 0; } /* - * Compute the vevent repetition end date from the repetition count. - * - * Extract from RFC2445: - * The COUNT rule part defines the number of occurrences at which to - * range-bound the recurrence. The "DTSTART" property value, if specified, - * counts as the first occurrence. - */ -static long ical_compute_rpt_until(long start, ical_rpt_t * rpt) -{ - switch (rpt->type) { - case RECUR_DAILY: - return date_sec_change(start, 0, rpt->freq * (rpt->count - 1)); - case RECUR_WEEKLY: - return date_sec_change(start, 0, - rpt->freq * WEEKINDAYS * (rpt->count - 1)); - case RECUR_MONTHLY: - return date_sec_change(start, rpt->freq * (rpt->count - 1), 0); - case RECUR_YEARLY: - return date_sec_change(start, - rpt->freq * 12 * (rpt->count - 1), 0); - default: - return 0; - } -} - -/* * Skip to the value part of an iCalendar content line. */ static char *ical_get_value(char *p) @@ -770,60 +919,160 @@ static char *ical_get_value(char *p) } /* + * Fill in the bymonth linked list from a comma-separated list of + * unsigned integers terminated by a space or end of string. + */ +static int ical_bymonth(llist_t *ll, char *cl) +{ + unsigned mon; + int *i, n; + + while (!(*cl == ' ' || *cl == '\0')) { + if (!(sscanf(cl, "%u%n", &mon, &n) == 1)) + return 0; + i = mem_malloc(sizeof(int)); + *i = mon; + LLIST_ADD(ll, i); + cl += n; + cl += (*cl == ','); + } + return 1; +} + +/* + * Fill in the bymonthday linked list from a comma-separated list of + * (signed) integers terminated by a space or end of string. + */ +static int ical_bymonthday(llist_t *ll, char *cl) +{ + int mday; + int *i, n; + + while (!(*cl == ' ' || *cl == '\0')) { + if (!(sscanf(cl, "%d%n", &mday, &n) == 1)) + return 0; + i = mem_malloc(sizeof(int)); + *i = mday; + LLIST_ADD(ll, i); + cl += n; + cl += (*cl == ','); + } + return 1; +} + +/* + * Fill in the bywday linked list from a comma-separated list of (ordered) + * weekday names (+1SU, MO, -5SA, 25TU, etc.) terminated by a space or end of + * string. + */ +static int ical_bywday(llist_t *ll, char *cl) +{ + int sign, order, wday, n, *i; + char *owd; + + while (!(*cl == ' ' || *cl == '\0')) { + /* find list separator */ + for (owd = cl; !(*cl == ',' || *cl == ' ' || *cl == '\0'); cl++) + ; + cl += (*cl == ','); + + if (!(sscanf(owd, "%d%n", &order, &n) == 1)) + order = n = 0; + sign = (order < 0) ? -1 : 1; + order *= sign; + owd += n; + if (starts_with(owd, "SU")) + wday = 0; + else if (starts_with(owd, "MO")) + wday = 1; + else if (starts_with(owd, "TU")) + wday = 2; + else if (starts_with(owd, "WE")) + wday = 3; + else if (starts_with(owd, "TH")) + wday = 4; + else if (starts_with(owd, "FR")) + wday = 5; + else if (starts_with(owd, "SA")) + wday = 6; + else + return 0; + + wday = sign * (wday + order * WEEKINDAYS); + i = mem_malloc(sizeof(int)); + *i = wday; + LLIST_ADD(ll, i); + } + return 1; +} + +/* * Read a recurrence rule from an iCalendar RRULE string. * + * RFC 5545, section 3.8.5.3: + * + * Property Name: RRULE + * + * Purpose: This property defines a rule or repeating pattern for + * recurring events, to-dos, journal entries, or time zone definitions. + * + * Value Type: RECUR + * + * RFC 5545, section 3.3.10: + * * Value Name: RECUR * * Purpose: This value type is used to identify properties that contain * a recurrence rule specification. * - * Formal Definition: The value type is defined by the following + * Format Definition: The value type is defined by the following * notation: * - * recur = "FREQ"=freq *( + * recur = recur-rule-part *( ";" recur-rule-part ) + * ; + * ; The rule parts are not ordered in any particular sequence. + * ; + * ; The FREQ rule part is REQUIRED, + * ; but MUST NOT occur more than once. + * ; + * ; The UNTIL or COUNT rule parts are OPTIONAL, + * ; but they MUST NOT occur in the same 'recur'. + * ; + * ; The other rule parts are OPTIONAL, + * ; but MUST NOT occur more than once. * - * ; either UNTIL or COUNT may appear in a 'recur', - * ; but UNTIL and COUNT MUST NOT occur in the same 'recur' - * - * ( ";" "UNTIL" "=" enddate ) / - * ( ";" "COUNT" "=" 1*DIGIT ) / - * - * ; the rest of these keywords are optional, - * ; but MUST NOT occur more than - * ; once - * - * ( ";" "INTERVAL" "=" 1*DIGIT ) / - * ( ";" "BYSECOND" "=" byseclist ) / - * ( ";" "BYMINUTE" "=" byminlist ) / - * ( ";" "BYHOUR" "=" byhrlist ) / - * ( ";" "BYDAY" "=" bywdaylist ) / - * ( ";" "BYMONTHDAY" "=" bymodaylist ) / - * ( ";" "BYYEARDAY" "=" byyrdaylist ) / - * ( ";" "BYWEEKNO" "=" bywknolist ) / - * ( ";" "BYMONTH" "=" bymolist ) / - * ( ";" "BYSETPOS" "=" bysplist ) / - * ( ";" "WKST" "=" weekday ) / - * ( ";" x-name "=" text ) - * ) -*/ -static ical_rpt_t *ical_read_rrule(FILE *log, char *rrulestr, + * recur-rule-part = ( "FREQ"=freq ) + * / ( "UNTIL" "=" enddate ) + * / ( "COUNT" "=" 1*DIGIT ) + * / ( "INTERVAL" "=" 1*DIGIT ) + * / ( "BYSECOND" "=" byseclist ) + * / ( "BYMINUTE" "=" byminlist ) + * / ( "BYHOUR" "=" byhrlist ) + * / ( "BYDAY" "=" bywdaylist ) + * / ( "BYMONTHDAY" "=" bymodaylist ) + * / ( "BYYEARDAY" "=" byyrdaylist ) + * / ( "BYWEEKNO" "=" bywknolist ) + * / ( "BYMONTH" "=" bymolist ) + * / ( "BYSETPOS" "=" bysplist ) + * / ( "WKST" "=" weekday ) + */ +static struct rpt *ical_read_rrule(FILE *log, char *rrulestr, unsigned *noskipped, const int itemline, - ical_vevent_e type) + ical_vevent_e type, + time_t start, + int *count) { - const char count[] = "COUNT="; - const char interv[] = "INTERVAL="; - char freqstr[BUFSIZ]; - unsigned interval; - ical_rpt_t *rpt; - char *p; + char freqstr[8], datestr[17]; + struct rpt *rpt; + char *p, *q; - /* See DTSTART. */ if (type == UNDEFINED) { ical_log(log, ICAL_VEVENT, itemline, _("need DTSTART to determine event type.")); return NULL; } + p = ical_get_value(rrulestr); if (!p) { ical_log(log, ICAL_VEVENT, itemline, @@ -831,69 +1080,136 @@ static ical_rpt_t *ical_read_rrule(FILE *log, char *rrulestr, (*noskipped)++; return NULL; } + /* Prepare for scanf(): replace semicolons by spaces. */ + for (q = p; (q = strchr(q, ';')); *q = ' ', q++) + ; + + rpt = mem_malloc(sizeof(struct rpt)); + memset(rpt, 0, sizeof(struct rpt)); + LLIST_INIT(&rpt->bymonth); + LLIST_INIT(&rpt->bywday); + LLIST_INIT(&rpt->bymonthday); - rpt = mem_malloc(sizeof(ical_rpt_t)); - memset(rpt, 0, sizeof(ical_rpt_t)); - if (sscanf(p, "FREQ=%s", freqstr) != 1) { + /* FREQ rule part */ + if ((p = strstr(rrulestr, "FREQ="))) { + if (sscanf(p, "FREQ=%7s", freqstr) != 1) { + ical_log(log, ICAL_VEVENT, itemline, + _("frequency not set in rrule.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + } else { ical_log(log, ICAL_VEVENT, itemline, - _("recurrence frequency not found.")); + _("frequency absent in rrule.")); (*noskipped)++; mem_free(rpt); return NULL; } - if (starts_with(freqstr, "DAILY")) { + if (!strcmp(freqstr, "DAILY")) rpt->type = RECUR_DAILY; - } else if (starts_with(freqstr, "WEEKLY")) { + else if (!strcmp(freqstr, "WEEKLY")) rpt->type = RECUR_WEEKLY; - } else if (starts_with(freqstr, "MONTHLY")) { + else if (!strcmp(freqstr, "MONTHLY")) rpt->type = RECUR_MONTHLY; - } else if (starts_with(freqstr, "YEARLY")) { + else if (!strcmp(freqstr, "YEARLY")) rpt->type = RECUR_YEARLY; - } else { + else { + ical_log(log, ICAL_VEVENT, itemline, + _("rrule frequency not supported.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + + /* INTERVAL rule part */ + rpt->freq = 1; + if ((p = strstr(rrulestr, "INTERVAL="))) { + if (sscanf(p, "INTERVAL=%d", &rpt->freq) != 1) { + ical_log(log, ICAL_VEVENT, itemline, _("invalid interval.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + } + + /* UNTIL and COUNT rule parts */ + if (strstr(rrulestr, "UNTIL=") && strstr(rrulestr, "COUNT=")) { ical_log(log, ICAL_VEVENT, itemline, - _("recurrence frequency not recognized.")); + _("either until or count.")); (*noskipped)++; mem_free(rpt); return NULL; } + if ((p = strstr(rrulestr, "UNTIL="))) { + if (sscanf(p, "UNTIL=%16s", datestr) != 1) { + ical_log(log, ICAL_VEVENT, itemline, + _("missing until value.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + rpt->until = ical_datetime2time_t(datestr, NULL, type); + if (!rpt->until) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid until format.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + } + /* - * The UNTIL rule part defines a date-time value which bounds the - * recurrence rule in an inclusive manner. If not present, and the - * COUNT rule part is also not present, the RRULE is considered to - * repeat forever. - - * The COUNT rule part defines the number of occurrences at which to - * range-bound the recurrence. The "DTSTART" property value, if - * specified, counts as the first occurrence. + * COUNT is converted to UNTIL in ical_read_event() once all recurrence + * parameters are known. */ - if ((p = strstr(rrulestr, "UNTIL")) != NULL) { - rpt->until = ical_datetime2time_t(strchr(p, '=') + 1, type); - if (!(rpt->until)) { + if ((p = strstr(rrulestr, "COUNT="))) { + p = strchr(p, '=') + 1; + if (!(sscanf(p, "%d", count) == 1 && *count)) { ical_log(log, ICAL_VEVENT, itemline, - _("invalid until format.")); + _("invalid count value.")); (*noskipped)++; mem_free(rpt); return NULL; } - } else { - unsigned cnt; - char *countstr; - - rpt->until = 0; - if ((countstr = strstr(rrulestr, count))) { - countstr += sizeof(count) - 1; - if (sscanf(countstr, "%u", &cnt) == 1) - rpt->count = cnt; + } + + /* BYMONTH rule part */ + if ((p = strstr(rrulestr, "BYMONTH="))) { + p = strchr(p, '=') + 1; + if (!ical_bymonth(&rpt->bymonth, p)) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid bymonth list.")); + (*noskipped)++; + mem_free(rpt); + return NULL; } } - rpt->freq = 1; - if ((p = strstr(rrulestr, interv))) { - p += sizeof(interv) - 1; - if (sscanf(p, "%u", &interval) == 1) - rpt->freq = interval; + /* BYMONTHDAY rule part */ + if ((p = strstr(rrulestr, "BYMONTHDAY="))) { + p = strchr(p, '=') + 1; + if (!ical_bymonthday(&rpt->bymonthday, p)) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid bymonthday list.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + } + + /* BYDAY rule part */ + if ((p = strstr(rrulestr, "BYDAY="))) { + p = strchr(p, '=') + 1; + if (!ical_bywday(&rpt->bywday, p)) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid byday list.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } } return rpt; @@ -915,47 +1231,42 @@ static int ical_read_exdate(llist_t * exc, FILE * log, char *exstr, unsigned *noskipped, const int itemline, ical_vevent_e type) { - char *p, *q; + char *p, *q, *tzid = NULL; time_t t; int n; - /* See DTSTART. */ - if (type == UNDEFINED) { - ical_log(log, ICAL_VEVENT, itemline, - _("need DTSTART to determine event type.")); - (*noskipped)++; - return 0; - } if (type != ical_get_type(exstr)) { ical_log(log, ICAL_VEVENT, itemline, _("invalid exception date value type.")); - (*noskipped)++; - return 0; + goto cleanup; } p = ical_get_value(exstr); if (!p) { ical_log(log, ICAL_VEVENT, itemline, _("malformed exceptions line.")); - (*noskipped)++; - return 0; + goto cleanup; } - + tzid = ical_get_tzid(exstr); /* Count the exceptions and replace commas by zeroes */ for (q = p, n = 1; (q = strchr(q, ',')); *q = '\0', q++, n++) ; while (n) { - if (!(t = ical_datetime2time_t(p, type))) { + if (!(t = ical_datetime2time_t(p, tzid, type))) { ical_log(log, ICAL_VEVENT, itemline, _("invalid exception.")); - (*noskipped)++; - return 0; + goto cleanup; } ical_add_exc(exc, t); p = strchr(p, '\0') + 1; n--; } - return 1; + +cleanup: + (*noskipped)++; + if (tzid) + mem_free(tzid); + return 0; } /* @@ -967,7 +1278,7 @@ static char *ical_read_note(char *line, ical_property_e property, unsigned *nosk FILE * log) { const int EOL = 1, - INDENT = (property != DESCRIPTION); + IND = (property != DESCRIPTION); char *p, *pname, *notestr; switch (property) { @@ -980,9 +1291,6 @@ static char *ical_read_note(char *line, ical_property_e property, unsigned *nosk case COMMENT: pname = "comment"; break; - case STATUS: - pname = "status"; - break; default: pname = "no property"; @@ -997,7 +1305,7 @@ static char *ical_read_note(char *line, ical_property_e property, unsigned *nosk goto leave; } - notestr = ical_unformat_line(p, EOL, INDENT); + notestr = ical_unformat_line(p, EOL, IND); if (!notestr) { asprintf(&p, _("malformed %s."), pname); ical_log(log, item_type, itemline, p); @@ -1013,7 +1321,7 @@ static char *ical_read_summary(char *line, unsigned *noskipped, ical_types_e item_type, const int itemline, FILE * log) { - const int EOL = 0, INDENT = 0; + const int EOL = 0, IND = 0; char *p, *summary = NULL; p = ical_get_value(line); @@ -1023,7 +1331,7 @@ static char *ical_read_summary(char *line, unsigned *noskipped, goto leave; } - summary = ical_unformat_line(p, EOL, INDENT); + summary = ical_unformat_line(p, EOL, IND); if (!summary) { ical_log(log, item_type, itemline, _("malformed summary.")); (*noskipped)++; @@ -1031,12 +1339,9 @@ static char *ical_read_summary(char *line, unsigned *noskipped, } /* An event summary is one line only. */ - if (strchr(summary, '\n')) { - ical_log(log, item_type, itemline, _("line break in summary.")); - (*noskipped)++; - mem_free(summary); - summary = NULL; - } + for (p = summary; *p; p++) + if (*p == '\n') + *p = ' '; leave: return summary; } @@ -1050,22 +1355,25 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, const int ITEMLINE = *lineno - !feof(fdi); ical_vevent_e vevent_type; ical_property_e property; - char *p, *note = NULL, *comment; - const char *SEPARATOR = "-- \n"; - struct string s; + char *p, *note, *tzid; + char *dtstart, *dtend, *duration, *rrule; + struct string s, exdate; struct { llist_t exc; - ical_rpt_t *rpt; - char *mesg, *desc, *loc, *comm, *stat, *note; - long start, end, dur; + struct rpt *rpt; + int count; + char *mesg, *desc, *loc, *comm, *imp, *note; + time_t start, end; + long dur; int has_alarm; } vevent; - int skip_alarm, has_note, separator; + int skip_alarm, has_note, separator, has_exdate; vevent_type = UNDEFINED; memset(&vevent, 0, sizeof vevent); LLIST_INIT(&vevent.exc); - skip_alarm = has_note = separator = 0; + note = dtstart = dtend = duration = rrule = NULL; + skip_alarm = has_note = separator = has_exdate =0; while (ical_readline(fdi, buf, lstore, lineno)) { note = NULL; property = NO_PROPERTY; @@ -1079,31 +1387,136 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, continue; } if (starts_with_ci(buf, "END:VEVENT")) { - if (!vevent.mesg) { + /* DTSTART and related properties (picked up earlier). */ + if (!dtstart) { ical_log(log, ICAL_VEVENT, ITEMLINE, - _("could not retrieve item summary.")); + _("item start date not defined.")); goto skip; } - if (vevent.start == 0) { + vevent_type = ical_get_type(dtstart); + if ((tzid = ical_get_tzid(dtstart)) && + vevent_type == APPOINTMENT) { + if (vevent.imp) { + asprintf(&p, "%s, TZID=%s", + vevent.imp, tzid); + mem_free(vevent.imp); + vevent.imp = p; + } else + asprintf(&vevent.imp, "TZID=%s", tzid); + has_note = separator = 1; + } + p = ical_get_value(dtstart); + if (!p) { ical_log(log, ICAL_VEVENT, ITEMLINE, - _("item start date is not defined.")); + _("malformed start time line.")); goto skip; } - if (vevent_type == APPOINTMENT && vevent.dur == 0) { - if (vevent.end != 0) { - vevent.dur = vevent.end - vevent.start; - } - - if (vevent.dur < 0) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("item has a negative duration.")); - goto skip; - } + vevent.start = ical_datetime2time_t(p, tzid, vevent_type); + if (tzid) { + mem_free(tzid); + tzid = NULL; + } + if (!vevent.start) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("invalid or malformed event " + "start time.")); + goto skip; + } + /* DTEND */ + if (!dtend) + goto duration; + if (vevent_type != ical_get_type(dtend)) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("invalid end time value type.")); + goto skip; + } + tzid = ical_get_tzid(dtend); + p = ical_get_value(dtend); + if (!p) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("malformed end time line.")); + goto skip; + } + vevent.end = ical_datetime2time_t(p, tzid, vevent_type); + if (tzid) { + mem_free(tzid); + tzid = NULL; + } + if (!vevent.end) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("malformed event end time.")); + goto skip; + } + if (vevent.end <= vevent.start) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("end must be later than start.")); + goto skip; + } + duration: + if (!duration) + goto rrule; + if (vevent.end) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("either end or duration.")); + goto skip; + } + p = ical_get_value(duration); + if (!p) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("malformed duration line.")); + goto skip; + } + vevent.dur = ical_dur2long(p, vevent_type); + if (!vevent.dur) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("invalid duration.")); + goto skip; } - if (vevent.rpt && vevent.rpt->count) { - vevent.rpt->until = - ical_compute_rpt_until(vevent.start, - vevent.rpt); + rrule: + if (!rrule) + goto exdate; + vevent.rpt = ical_read_rrule(log, rrule, noskipped, + ITEMLINE, vevent_type, vevent.start, + &vevent.count); + if (!vevent.rpt) + goto cleanup; + exdate: + if (!has_exdate) + goto duration_end; + if (!rrule) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("exception date, but no recurrence " + "rule.")); + goto skip; + } + if (!ical_read_exdate(&vevent.exc, log, exdate.buf, + noskipped, ITEMLINE, vevent_type)) + goto cleanup; + duration_end: + /* An APPOINTMENT must always have a duration. */ + if (vevent_type == APPOINTMENT && !vevent.dur) { + vevent.dur = vevent.end ? + vevent.end - vevent.start : + 0; + } + /* An EVENT must always have an end. */ + if (vevent_type == EVENT) { + if (!vevent.end) + vevent.end = vevent.start + vevent.dur; + vevent.dur = vevent.end - vevent.start; + if (vevent.dur > DAYINSEC) { + /* Add note on multi-day events. */ + char *md = _("multi-day event changed " + "to one-day event"); + if (vevent.imp) { + asprintf(&p, "%s, %s", + vevent.imp, md); + mem_free(vevent.imp); + vevent.imp = p; + } else + asprintf(&vevent.imp, "%s", md); + has_note = separator = 1; + } } if (has_note) { /* Construct string with note file contents. */ @@ -1111,41 +1524,75 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, if (vevent.desc) { string_catf(&s, "%s", vevent.desc); mem_free(vevent.desc); - if (separator) - string_catf(&s, SEPARATOR); + vevent.desc = NULL; } + if (separator) + string_catf(&s, SEPARATOR); if (vevent.loc) { string_catf(&s, _("Location: %s"), vevent.loc); mem_free(vevent.loc); + vevent.loc = NULL; } if (vevent.comm) { string_catf(&s, _("Comment: %s"), vevent.comm); mem_free(vevent.comm); + vevent.comm = NULL; } - if (vevent.stat) { - string_catf(&s, _("Status: %s"), - vevent.stat); - mem_free(vevent.stat); + if (vevent.imp) { + string_catf(&s, ("Import: %s\n"), + vevent.imp); + mem_free(vevent.imp); + vevent.imp = NULL; } vevent.note = generate_note(string_buf(&s)); mem_free(s.buf); } + if (vevent.rpt) { + time_t day, until; + long dur; + char *msg; + + dur = vevent_type == EVENT ? -1 : vevent.dur; + day = DAY(vevent.start); + msg = _("rrule does not match start day (%s)."); + + if (vevent.count) { + recur_nth_occurrence(vevent.start, + dur, + vevent.rpt, + &vevent.exc, + vevent.count, + &until); + vevent.rpt->until = until; + } + if (!recur_item_find_occurrence(vevent.start, + dur, + vevent.rpt, + NULL, + day, + NULL)) { + char *l = day_ins(&msg, vevent.start); + ical_log(log, ICAL_VEVENT, ITEMLINE, l); + mem_free(l); + goto skip; + } + } switch (vevent_type) { case APPOINTMENT: ical_store_apoint(vevent.mesg, vevent.note, - vevent.start, vevent.dur, - vevent.rpt, &vevent.exc, - vevent.has_alarm, fmt_apt, - fmt_rapt); + vevent.start, vevent.dur, + vevent.rpt, &vevent.exc, + vevent.has_alarm, + fmt_apt, fmt_rapt); (*noapoints)++; break; case EVENT: ical_store_event(vevent.mesg, vevent.note, - vevent.start, vevent.end, - vevent.rpt, &vevent.exc, - fmt_ev, fmt_rev); + vevent.start, vevent.end, + vevent.rpt, &vevent.exc, + fmt_ev, fmt_rev); (*noevents)++; break; case UNDEFINED: @@ -1158,84 +1605,28 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, } if (starts_with_ci(buf, "DTSTART")) { /* - * DTSTART has a value type: either DATE-TIME (by - * default) or DATE. Properties DTEND, DURATION and - * EXDATE and rrule part UNTIL must agree. - * Assume that DTSTART comes before the others even - * though RFC 5545 allows any order. + * DTSTART has a value type: either DATE-TIME or DATE. * In calcurse DATE-TIME implies an appointment, DATE an * event. + * Properties DTEND, DURATION and EXDATE and rrule part + * UNTIL must match the DTSTART value type. */ - vevent_type = ical_get_type(buf); - p = ical_get_value(buf); - if (!p) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("malformed start time line.")); - goto skip; - } - - vevent.start = ical_datetime2time_t(p, vevent_type); - if (!vevent.start) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("invalid or malformed event " - "start time.")); - goto skip; - } + asprintf(&dtstart, "%s", buf); } else if (starts_with_ci(buf, "DTEND")) { - /* See DTSTART. */ - if (vevent_type == UNDEFINED) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("need DTSTART to determine " - "event type.")); - goto skip; - } - if (vevent_type != ical_get_type(buf)) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("invalid end time value type.")); - goto skip; - } - p = ical_get_value(buf); - if (!p) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("malformed end time line.")); - goto skip; - } - - vevent.end = ical_datetime2time_t(p, vevent_type); - if (!vevent.end) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("malformed event end time.")); - goto skip; - } + asprintf(&dtend, "%s", buf); } else if (starts_with_ci(buf, "DURATION")) { - /* See DTSTART. */ - if (vevent_type == UNDEFINED) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("need DTSTART to determine " - "event type.")); - goto skip; - } - p = ical_get_value(buf); - if (!p) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("malformed duration line.")); - goto skip; - } - vevent.dur = ical_dur2long(p, vevent_type); - if (!vevent.dur) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("invalid duration.")); - goto skip; - } + asprintf(&duration, "%s", buf); } else if (starts_with_ci(buf, "RRULE")) { - vevent.rpt = ical_read_rrule(log, buf, noskipped, - ITEMLINE, vevent_type); - if (!vevent.rpt) - goto cleanup; + asprintf(&rrule, "%s", buf); } else if (starts_with_ci(buf, "EXDATE")) { - if (!ical_read_exdate(&vevent.exc, log, buf, noskipped, - ITEMLINE, vevent_type)) - goto cleanup; + if (!has_exdate) { + has_exdate = 1; + string_init(&exdate); + string_catf(&exdate, "%s", buf); + } else { + p = ical_get_value(buf); + string_catf(&exdate, ",%s", p); + } } else if (starts_with_ci(buf, "SUMMARY")) { vevent.mesg = ical_read_summary(buf, noskipped, ICAL_VEVENT, ITEMLINE, log); @@ -1249,15 +1640,14 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, property = LOCATION; } else if (starts_with_ci(buf, "COMMENT")) { property = COMMENT; - } else if (starts_with_ci(buf, "STATUS")) { - property = STATUS; } if (property) { note = ical_read_note(buf, property, noskipped, ICAL_VEVENT, ITEMLINE, log); if (!note) goto cleanup; - separator = (property != DESCRIPTION); + if (!separator) + separator = (property != DESCRIPTION); has_note = 1; } switch (property) { @@ -1280,28 +1670,13 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, case COMMENT: /* There may be more than one. */ if (vevent.comm) { - asprintf(&comment, "%sComment: %s", + asprintf(&p, "%sComment: %s", vevent.comm, note); mem_free(vevent.comm); - vevent.comm = comment; + vevent.comm = p; } else vevent.comm = note; break; - case STATUS: - if (vevent.stat) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("only one status allowed.")); - goto skip; - } - if (!(starts_with(note, "TENTATIVE") || - starts_with(note, "CONFIRMED") || - starts_with(note, "CANCELLED"))) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("invalid status value.")); - goto skip; - } - vevent.stat = note; - break; default: break; } @@ -1312,6 +1687,16 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, skip: (*noskipped)++; cleanup: + if (dtstart) + mem_free(dtstart); + if (dtend) + mem_free(dtend); + if (duration) + mem_free(duration); + if (rrule) + mem_free(rrule); + if (has_exdate) + mem_free(exdate.buf); if (note) mem_free(note); if (vevent.desc) @@ -1320,8 +1705,8 @@ cleanup: mem_free(vevent.loc); if (vevent.comm) mem_free(vevent.comm); - if (vevent.stat) - mem_free(vevent.stat); + if (vevent.imp) + mem_free(vevent.imp); if (vevent.mesg) mem_free(vevent.mesg); if (vevent.rpt) @@ -1335,17 +1720,17 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, { const int ITEMLINE = *lineno - !feof(fdi); ical_property_e property; - char *note = NULL, *comment; - const char *SEPARATOR = "-- \n"; + char *p, *note; struct string s; struct { - char *mesg, *desc, *loc, *comm, *stat, *note; + char *mesg, *desc, *loc, *comm, *note; int priority; int completed; } vtodo; int skip_alarm, has_note, separator; memset(&vtodo, 0, sizeof vtodo); + note = NULL; skip_alarm = has_note = separator = 0; while (ical_readline(fdi, buf, lstore, lineno)) { note = NULL; @@ -1371,23 +1756,21 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, if (vtodo.desc) { string_catf(&s, "%s", vtodo.desc); mem_free(vtodo.desc); - if (separator) - string_catf(&s, SEPARATOR); + vtodo.desc = NULL; } + if (separator) + string_catf(&s, SEPARATOR); if (vtodo.loc) { string_catf(&s, _("Location: %s"), vtodo.loc); mem_free(vtodo.loc); + vtodo.loc = NULL; } if (vtodo.comm) { string_catf(&s, _("Comment: %s"), vtodo.comm); mem_free(vtodo.comm); - } - if (vtodo.stat) { - string_catf(&s, _("Status: %s"), - vtodo.stat); - mem_free(vtodo.stat); + vtodo.comm = NULL; } vtodo.note = generate_note(string_buf(&s)); mem_free(s.buf); @@ -1407,7 +1790,6 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, } } else if (starts_with_ci(buf, "STATUS:COMPLETED")) { vtodo.completed = 1; - property = STATUS; } else if (starts_with_ci(buf, "SUMMARY")) { vtodo.mesg = ical_read_summary(buf, noskipped, ICAL_VTODO, @@ -1422,15 +1804,14 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, property = LOCATION; } else if (starts_with_ci(buf, "COMMENT")) { property = COMMENT; - } else if (starts_with_ci(buf, "STATUS")) { - property = STATUS; } if (property) { note = ical_read_note(buf, property, noskipped, ICAL_VTODO, ITEMLINE, log); if (!note) goto cleanup; - separator = (property != DESCRIPTION); + if (!separator) + separator = (property != DESCRIPTION); has_note = 1; } switch (property) { @@ -1453,29 +1834,13 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, case COMMENT: /* There may be more than one. */ if (vtodo.comm) { - asprintf(&comment, "%sComment: %s", + asprintf(&p, "%sComment: %s", vtodo.comm, note); mem_free(vtodo.comm); - vtodo.comm = comment; + vtodo.comm = p; } else vtodo.comm = note; break; - case STATUS: - if (vtodo.stat) { - ical_log(log, ICAL_VTODO, ITEMLINE, - _("only one status allowed.")); - goto skip; - } - if (!(starts_with(note, "NEEDS-ACTION") || - starts_with(note, "COMPLETED") || - starts_with(note, "IN-PROCESS") || - starts_with(note, "CANCELLED"))) { - ical_log(log, ICAL_VTODO, ITEMLINE, - _("invalid status value.")); - goto skip; - } - vtodo.stat = note; - break; default: break; } @@ -1494,8 +1859,6 @@ cleanup: mem_free(vtodo.loc); if (vtodo.comm) mem_free(vtodo.comm); - if (vtodo.stat) - mem_free(vtodo.stat); if (vtodo.mesg) mem_free(vtodo.mesg); } |