/* * Calcurse - text-based organizer * * Copyright (c) 2004-2020 calcurse Development Team * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * Send your feedback or comments to : misc@calcurse.org * Calcurse home page : http://calcurse.org * */ #include #include #include "calcurse.h" #define ICALDATEFMT "%Y%m%d" #define ICALDATETIMEFMT "%Y%m%dT%H%M%S" typedef enum { ICAL_VEVENT, ICAL_VTODO, ICAL_TYPES } ical_types_e; typedef enum { UNDEFINED, APPOINTMENT, EVENT } ical_vevent_e; typedef enum { NO_PROPERTY, SUMMARY, DESCRIPTION, LOCATION, COMMENT, STATUS } 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); static void ical_export_recur_apoints(FILE *, int); static void ical_export_apoints(FILE *, int); static void ical_export_todo(FILE *, int); 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) { char * p; fputs(field, stream); for (p = msg; *p; p++) { switch (*p) { case ',': case ';': case '\\': fprintf(stream, "\\%c", *p); break; default: fputc(*p, stream); break; } } fputc('\n', stream); } /* iCal alarm notification. */ static void ical_export_valarm(FILE * stream) { fputs("BEGIN:VALARM\n", stream); pthread_mutex_lock(&nbar.mutex); fprintf(stream, "TRIGGER:-P%dS\n", nbar.cntdwn); pthread_mutex_unlock(&nbar.mutex); fputs("ACTION:DISPLAY\n", stream); fputs("END:VALARM\n", stream); } /* 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); } /* Export footer. */ static void ical_export_footer(FILE * stream) { fputs("END:VCALENDAR\n", stream); } /* Export recurrent events. */ static void ical_export_recur_events(FILE * stream, int export_uid) { llist_item_t *i, *j; char ical_date[BUFSIZ]; 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 (LLIST_FIRST(&rev->exc)) { fputs("EXDATE:", stream); LLIST_FOREACH(&rev->exc, j) { struct excp *exc = LLIST_GET_DATA(j); date_sec2date_fmt(exc->st, ICALDATETIMEFMT, ical_date); fprintf(stream, "%s", ical_date); 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); } fputs("END:VEVENT\n", stream); } } /* Export events. */ static void ical_export_events(FILE * stream, int export_uid) { llist_item_t *i; char ical_date[BUFSIZ]; 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); fprintf(stream, "UID:%s\n", hash); mem_free(hash); } fputs("END:VEVENT\n", stream); } } /* Export recurrent appointments. */ static void ical_export_recur_apoints(FILE * stream, int export_uid) { llist_item_t *i, *j; char ical_datetime[BUFSIZ]; char ical_date[BUFSIZ]; LLIST_TS_LOCK(&recur_alist_p); LLIST_TS_FOREACH(&recur_alist_p, i) { struct recur_apoint *rapt = LLIST_TS_GET_DATA(i); date_sec2date_fmt(rapt->start, ICALDATETIMEFMT, ical_datetime); fputs("BEGIN:VEVENT\n", stream); fprintf(stream, "DTSTART:%s\n", ical_datetime); if (rapt->dur > 0) { fprintf(stream, "DURATION:P%ldDT%ldH%ldM%ldS\n", rapt->dur / DAYINSEC, (rapt->dur / HOURINSEC) % DAYINHOURS, (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); } 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); fputc(LLIST_NEXT(j) ? ',' : '\n', stream); } } ical_format_line(stream, "SUMMARY:", rapt->mesg); 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); } /* Export appointments. */ static void ical_export_apoints(FILE * stream, int export_uid) { llist_item_t *i; char ical_datetime[BUFSIZ]; LLIST_TS_LOCK(&alist_p); LLIST_TS_FOREACH(&alist_p, i) { struct apoint *apt = LLIST_TS_GET_DATA(i); 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", apt->dur / DAYINSEC, (apt->dur / HOURINSEC) % DAYINHOURS, (apt->dur / MININSEC) % HOURINMIN, apt->dur % MININSEC); } ical_format_line(stream, "SUMMARY:", apt->mesg); 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); } /* Export todo items. */ static void ical_export_todo(FILE * stream, int export_uid) { llist_item_t *i; 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); fprintf(stream, "UID:%s\n", hash); mem_free(hash); } fputs("END:VTODO\n", stream); } } /* Print a header to describe import log report format. */ static void ical_log_init(const char *file, FILE * log, int major, int minor) { const char *header = "+-------------------------------------------------------------------+\n" "| Calcurse icalendar import log. |\n" "| |\n" "| Import from icalendar file |\n" "| %-60s|\n" "| version %d.%d at %s. |\n" "| |\n" "| Items which could not be imported are described below. |\n" "| The log line format is as follows: |\n" "| |\n" "| TYPE [LINE]: DESCRIPTION |\n" "| |\n" "| where: |\n" "| * TYPE is the item type, 'VEVENT' or 'VTODO' |\n" "| * LINE is the line in the import file where the item begins |\n" "| * DESCRIPTION explains why the item could not be imported |\n" "+-------------------------------------------------------------------+\n\n"; char *date, *fmt; asprintf(&fmt, "%s %s", DATEFMT(conf.input_datefmt), "%H:%M"); date = date_sec2date_str(now(), fmt); if (log) fprintf(log, header, file, major, minor, date); mem_free(fmt); mem_free(date); } /* * Used to build a report of the import process. * The icalendar item for which a problem occurs is mentioned (by giving its * first line inside the icalendar file), together with a message describing the * problem. */ static void ical_log(FILE * log, ical_types_e type, unsigned lineno, char *msg) { const char *typestr[ICAL_TYPES] = { "VEVENT", "VTODO" }; RETURN_IF(type < 0 || type >= ICAL_TYPES, _("unknown ical type")); if (!log) return; fprintf(log, "%s [%d]: %s\n", typestr[type], lineno, msg); } static void ical_store_todo(int priority, int completed, char *mesg, char *note, const char *fmt_todo) { struct todo *todo = todo_add(mesg, priority, completed, note); if (fmt_todo) print_todo(fmt_todo, todo); mem_free(mesg); erase_note(¬e); } static void ical_store_event(char *mesg, char *note, long day, long end, ical_rpt_t *irpt, 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 (fmt_rev) print_recur_event(fmt_rev, day, rev); goto cleanup; } if (end == 0 || end - day <= DAYINSEC) { ev = event_new(mesg, note, day, EVENTID); if (fmt_ev) print_event(fmt_ev, day, ev); goto cleanup; } /* * 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. */ 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); if (fmt_rev) print_recur_event(fmt_rev, day, rev); cleanup: mem_free(mesg); erase_note(¬e); } static void ical_store_apoint(char *mesg, char *note, long start, long dur, ical_rpt_t * irpt, llist_t * exc, int has_alarm, const char *fmt_apt, const char *fmt_rapt) { char state = 0L; struct apoint *apt; struct recur_apoint *rapt; 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 (fmt_rapt) print_recur_apoint(fmt_rapt, start, rapt->start, rapt); } else { apt = apoint_new(mesg, note, start, dur, state); if (fmt_apt) print_apoint(fmt_apt, start, apt); } mem_free(mesg); erase_note(¬e); } /* * Return an allocated string containing the decoded 'line' or NULL on error. * The last arguments are used to format a note file entry. * The line is assumed to be the value part of a content line of type TEXT or * INTEGER (RFC 5545, 3.3.11 and 3.3.8) without list or field separators (3.1.1). */ 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++) { switch (*p) { case '\\': switch (*(p + 1)) { case 'N': case 'n': string_catf(&s, "%c", '\n'); if (indentation) string_catf(&s, "%s", INDENT); p++; break; case '\\': case ';': case ',': string_catf(&s, "%c", *(p + 1)); p++; break; default: mem_free(s.buf); return NULL; } break; case ',': case ';': /* No list or field separator allowed. */ mem_free(s.buf); return NULL; default: string_catf(&s, "%c", *p); break; } } /* Add the final EOL removed by ical_readline(). */ if (eol) string_catf(&s, "\n"); return string_buf(&s); } static void ical_readline_init(FILE * fdi, char *buf, char *lstore, unsigned *ln) { char *eol; *buf = *lstore = '\0'; if (fgets(lstore, BUFSIZ, fdi)) { (*ln)++; if ((eol = strchr(lstore, '\n')) != NULL) { if (*(eol - 1) == '\r') *(eol - 1) = '\0'; else *eol = '\0'; } } } static int ical_readline(FILE * fdi, char *buf, char *lstore, unsigned *ln) { char *eol; strncpy(buf, lstore, BUFSIZ); while (fgets(lstore, BUFSIZ, fdi) != NULL) { (*ln)++; if ((eol = strchr(lstore, '\n')) != NULL) { if (*(eol - 1) == '\r') *(eol - 1) = '\0'; else *eol = '\0'; } if (*lstore != SPACE && *lstore != TAB) break; strncat(buf, lstore + 1, BUFSIZ - strlen(buf) - 1); } if (feof(fdi)) { *lstore = '\0'; if (*buf == '\0') return 0; } return 1; } static int ical_chk_header(FILE * fd, char *buf, char *lstore, unsigned *lineno, int *major, int *minor) { if (!ical_readline(fd, buf, lstore, lineno)) return 0; if (!starts_with_ci(buf, "BEGIN:VCALENDAR")) return 0; while (!sscanf(buf, "VERSION:%d.%d", major, minor)) { if (!ical_readline(fd, buf, lstore, lineno)) return 0; } return 1; } /* * Return event type from a DTSTART/DTEND/EXDATE property. */ static ical_vevent_e ical_get_type(char *c_line) { const char vparam[] = ";VALUE=DATE"; char *p; if ((p = strstr(c_line, vparam))) { p += sizeof(vparam) - 1; if (*p == ':' || *p == ';') return EVENT; } return APPOINTMENT; } /* * iCalendar date-time format is based on the ISO 8601 complete * representation. It should be something like : DATE 'T' TIME * 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. */ static time_t ical_datetime2time_t(char *datestr, ical_vevent_e type) { const int INVALID = 0, DATE = 3, DATETIME = 6, DATETIMEZ = 7; struct date date; unsigned hour, min, sec; char c; 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') format = DATETIME; if (format == DATE && type == EVENT) return date2sec(date, 0, 0); else if (format == DATETIME && type == APPOINTMENT) return date2sec(date, hour, min); else if (format == DATETIMEZ && type == APPOINTMENT) return utcdate2sec(date, hour, min); return 0; } static long ical_durtime2long(char *timestr) { char *p = timestr; int bytes_read; unsigned hour = 0, min = 0, sec = 0; if (*p != 'T') return 0; p++; if (strchr(p, 'H')) { if (sscanf(p, "%uH%n", &hour, &bytes_read) != 1) return 0; p += bytes_read; } if (strchr(p, 'M')) { if (sscanf(p, "%uM%n", &min, &bytes_read) != 1) return 0; p += bytes_read; } if (strchr(p, 'S')) { if (sscanf(p, "%uS%n", &sec, &bytes_read) != 1) return 0; p += bytes_read; } return hour * HOURINSEC + min * MININSEC + sec; } /* * Extract from RFC2445 section 3.8.2.5: * * Property Name: DURATION * * Purpose: This property specifies a positive duration of time. * * Value Type: DURATION * * and section 3.3.6: * * Value Name: DURATION * * Purpose: This value type is used to identify properties that contain * a duration of time. * * Format Definition: The value type is defined by the following notation: * * dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week) * dur-date = dur-day [dur-time] * dur-time = "T" (dur-hour / dur-minute / dur-second) * dur-week = 1*DIGIT "W" * dur-hour = 1*DIGIT "H" [dur-minute] * dur-minute = 1*DIGIT "M" [dur-second] * dur-second = 1*DIGIT "S" * dur-day = 1*DIGIT "D" * * Example: A duration of 15 days, 5 hours and 20 seconds would be: * P15DT5H0M20S * A duration of 7 weeks would be: * P7W */ static long ical_dur2long(char *durstr, ical_vevent_e type) { char *p = durstr, c; int bytes_read; unsigned week, day; if (*p == '-') return 0; if (*p == '+') p++; if (*p != 'P') return 0; p++; if (*p == 'T' && type == APPOINTMENT) /* dur-time */ return ical_durtime2long(p); else if (sscanf(p, "%u%c", &week, &c) == 2 && c == 'W') /* dur-week */ return week * WEEKINDAYS * DAYINSEC; 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 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) { if (!(p && *p)) return NULL; for (; *p != ':'; p++) { if (*p == '"') for (p++; *p && *p != '"'; p++); if (!*p) return NULL; } return p + 1; } /* * Read a recurrence rule from an iCalendar RRULE string. * * 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 * notation: * * recur = "FREQ"=freq *( * * ; 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, unsigned *noskipped, const int itemline, ical_vevent_e type) { const char count[] = "COUNT="; const char interv[] = "INTERVAL="; char freqstr[BUFSIZ]; unsigned interval; ical_rpt_t *rpt; char *p; /* 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, _("malformed recurrence line.")); (*noskipped)++; return NULL; } rpt = mem_malloc(sizeof(ical_rpt_t)); memset(rpt, 0, sizeof(ical_rpt_t)); if (sscanf(p, "FREQ=%s", freqstr) != 1) { ical_log(log, ICAL_VEVENT, itemline, _("recurrence frequency not found.")); (*noskipped)++; mem_free(rpt); return NULL; } if (starts_with(freqstr, "DAILY")) { rpt->type = RECUR_DAILY; } else if (starts_with(freqstr, "WEEKLY")) { rpt->type = RECUR_WEEKLY; } else if (starts_with(freqstr, "MONTHLY")) { rpt->type = RECUR_MONTHLY; } else if (starts_with(freqstr, "YEARLY")) { rpt->type = RECUR_YEARLY; } else { ical_log(log, ICAL_VEVENT, itemline, _("recurrence frequency not recognized.")); (*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. */ if ((p = strstr(rrulestr, "UNTIL")) != NULL) { rpt->until = ical_datetime2time_t(strchr(p, '=') + 1, type); if (!(rpt->until)) { ical_log(log, ICAL_VEVENT, itemline, _("invalid until format.")); (*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; } } rpt->freq = 1; if ((p = strstr(rrulestr, interv))) { p += sizeof(interv) - 1; if (sscanf(p, "%u", &interval) == 1) rpt->freq = interval; } return rpt; } static void ical_add_exc(llist_t * exc_head, time_t date) { struct excp *exc = mem_malloc(sizeof(struct excp)); exc->st = date; LLIST_ADD(exc_head, exc); } /* * This property defines a comma-separated list of date/time exceptions for a * recurring calendar component. */ static int ical_read_exdate(llist_t * exc, FILE * log, char *exstr, unsigned *noskipped, const int itemline, ical_vevent_e type) { char *p, *q; 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; } p = ical_get_value(exstr); if (!p) { ical_log(log, ICAL_VEVENT, itemline, _("malformed exceptions line.")); (*noskipped)++; return 0; } /* 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))) { ical_log(log, ICAL_VEVENT, itemline, _("invalid exception.")); (*noskipped)++; return 0; } ical_add_exc(exc, t); p = strchr(p, '\0') + 1; n--; } return 1; } /* * Return an allocated string containing a property value to be written in a * note file or NULL on error. */ static char *ical_read_note(char *line, ical_property_e property, unsigned *noskipped, ical_types_e item_type, const int itemline, FILE * log) { const int EOL = 1, INDENT = (property != DESCRIPTION); char *p, *pname, *notestr; switch (property) { case DESCRIPTION: pname = "description"; break; case LOCATION: pname = "location"; break; case COMMENT: pname = "comment"; break; case STATUS: pname = "status"; break; default: pname = "no property"; } p = ical_get_value(line); if (!p) { asprintf(&p, _("malformed %s line."), pname); ical_log(log, item_type, itemline, p); mem_free(p); (*noskipped)++; notestr = NULL; goto leave; } notestr = ical_unformat_line(p, EOL, INDENT); if (!notestr) { asprintf(&p, _("malformed %s."), pname); ical_log(log, item_type, itemline, p); mem_free(p); (*noskipped)++; } leave: return notestr; } /* Returns an allocated string containing the ical item summary. */ 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; char *p, *summary = NULL; p = ical_get_value(line); if (!p) { ical_log(log, item_type, itemline, _("malformed summary line.")); (*noskipped)++; goto leave; } summary = ical_unformat_line(p, EOL, INDENT); if (!summary) { ical_log(log, item_type, itemline, _("malformed summary.")); (*noskipped)++; goto leave; } /* 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; } leave: return summary; } static void ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, unsigned *noapoints, unsigned *noskipped, char *buf, char *lstore, unsigned *lineno, const char *fmt_ev, const char *fmt_rev, const char *fmt_apt, const char *fmt_rapt) { 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; struct { llist_t exc; ical_rpt_t *rpt; char *mesg, *desc, *loc, *comm, *stat, *note; long start, end, dur; int has_alarm; } vevent; int skip_alarm, has_note, separator; vevent_type = UNDEFINED; memset(&vevent, 0, sizeof vevent); LLIST_INIT(&vevent.exc); skip_alarm = has_note = separator = 0; while (ical_readline(fdi, buf, lstore, lineno)) { note = NULL; property = NO_PROPERTY; if (skip_alarm) { /* * Need to skip VALARM properties because some keywords * could interfere, such as DURATION, SUMMARY,.. */ if (starts_with_ci(buf, "END:VALARM")) skip_alarm = 0; continue; } if (starts_with_ci(buf, "END:VEVENT")) { if (!vevent.mesg) { ical_log(log, ICAL_VEVENT, ITEMLINE, _("could not retrieve item summary.")); goto skip; } if (vevent.start == 0) { ical_log(log, ICAL_VEVENT, ITEMLINE, _("item start date is not defined.")); 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; } } if (vevent.rpt && vevent.rpt->count) { vevent.rpt->until = ical_compute_rpt_until(vevent.start, vevent.rpt); } if (has_note) { /* Construct string with note file contents. */ string_init(&s); if (vevent.desc) { string_catf(&s, "%s", vevent.desc); mem_free(vevent.desc); if (separator) string_catf(&s, SEPARATOR); } if (vevent.loc) { string_catf(&s, _("Location: %s"), vevent.loc); mem_free(vevent.loc); } if (vevent.comm) { string_catf(&s, _("Comment: %s"), vevent.comm); mem_free(vevent.comm); } if (vevent.stat) { string_catf(&s, _("Status: %s"), vevent.stat); mem_free(vevent.stat); } vevent.note = generate_note(string_buf(&s)); mem_free(s.buf); } 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); (*noapoints)++; break; case EVENT: ical_store_event(vevent.mesg, vevent.note, vevent.start, vevent.end, vevent.rpt, &vevent.exc, fmt_ev, fmt_rev); (*noevents)++; break; case UNDEFINED: ical_log(log, ICAL_VEVENT, ITEMLINE, _("item could not be identified.")); goto skip; break; } return; } 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. * In calcurse DATE-TIME implies an appointment, DATE an * event. */ 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; } } 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; } } 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; } } else if (starts_with_ci(buf, "RRULE")) { vevent.rpt = ical_read_rrule(log, buf, noskipped, ITEMLINE, vevent_type); if (!vevent.rpt) goto cleanup; } else if (starts_with_ci(buf, "EXDATE")) { if (!ical_read_exdate(&vevent.exc, log, buf, noskipped, ITEMLINE, vevent_type)) goto cleanup; } else if (starts_with_ci(buf, "SUMMARY")) { vevent.mesg = ical_read_summary(buf, noskipped, ICAL_VEVENT, ITEMLINE, log); if (!vevent.mesg) goto cleanup; } else if (starts_with_ci(buf, "BEGIN:VALARM")) { skip_alarm = vevent.has_alarm = 1; } else if (starts_with_ci(buf, "DESCRIPTION")) { property = DESCRIPTION; } else if (starts_with_ci(buf, "LOCATION")) { 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); has_note = 1; } switch (property) { case DESCRIPTION: if (vevent.desc) { ical_log(log, ICAL_VEVENT, ITEMLINE, _("only one description allowed.")); goto skip; } vevent.desc = note; break; case LOCATION: if (vevent.loc) { ical_log(log, ICAL_VEVENT, ITEMLINE, _("only one location allowed.")); goto skip; } vevent.loc = note; break; case COMMENT: /* There may be more than one. */ if (vevent.comm) { asprintf(&comment, "%sComment: %s", vevent.comm, note); mem_free(vevent.comm); vevent.comm = comment; } 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; } } ical_log(log, ICAL_VEVENT, ITEMLINE, _("The ical file seems to be malformed. " "The end of item was not found.")); skip: (*noskipped)++; cleanup: if (note) mem_free(note); if (vevent.desc) mem_free(vevent.desc); if (vevent.loc) mem_free(vevent.loc); if (vevent.comm) mem_free(vevent.comm); if (vevent.stat) mem_free(vevent.stat); if (vevent.mesg) mem_free(vevent.mesg); if (vevent.rpt) mem_free(vevent.rpt); LLIST_FREE(&vevent.exc); } static void ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, char *buf, char *lstore, unsigned *lineno, const char *fmt_todo) { const int ITEMLINE = *lineno - !feof(fdi); ical_property_e property; char *note = NULL, *comment; const char *SEPARATOR = "-- \n"; struct string s; struct { char *mesg, *desc, *loc, *comm, *stat, *note; int priority; int completed; } vtodo; int skip_alarm, has_note, separator; memset(&vtodo, 0, sizeof vtodo); skip_alarm = has_note = separator = 0; while (ical_readline(fdi, buf, lstore, lineno)) { note = NULL; property = NO_PROPERTY; if (skip_alarm) { /* * Need to skip VALARM properties because some keywords * could interfere, such as DURATION, SUMMARY,.. */ if (starts_with_ci(buf, "END:VALARM")) skip_alarm = 0; continue; } if (starts_with_ci(buf, "END:VTODO")) { if (!vtodo.mesg) { ical_log(log, ICAL_VTODO, ITEMLINE, _("could not retrieve item summary.")); goto cleanup; } if (has_note) { /* Construct string with note file contents. */ string_init(&s); if (vtodo.desc) { string_catf(&s, "%s", vtodo.desc); mem_free(vtodo.desc); if (separator) string_catf(&s, SEPARATOR); } if (vtodo.loc) { string_catf(&s, _("Location: %s"), vtodo.loc); mem_free(vtodo.loc); } 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.note = generate_note(string_buf(&s)); mem_free(s.buf); } ical_store_todo(vtodo.priority, vtodo.completed, vtodo.mesg, vtodo.note, fmt_todo); (*notodos)++; return; } if (starts_with_ci(buf, "PRIORITY:")) { sscanf(buf, "PRIORITY:%d\n", &vtodo.priority); if (vtodo.priority < 0 || vtodo.priority > 9) { ical_log(log, ICAL_VTODO, ITEMLINE, _("item priority is invalid " "(must be between 0 and 9).")); goto skip; } } 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, ITEMLINE, log); if (!vtodo.mesg) goto cleanup; } else if (starts_with_ci(buf, "BEGIN:VALARM")) { skip_alarm = 1; } else if (starts_with_ci(buf, "DESCRIPTION")) { property = DESCRIPTION; } else if (starts_with_ci(buf, "LOCATION")) { 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); has_note = 1; } switch (property) { case DESCRIPTION: if (vtodo.desc) { ical_log(log, ICAL_VTODO, ITEMLINE, _("only one description allowed.")); goto skip; } vtodo.desc = note; break; case LOCATION: if (vtodo.loc) { ical_log(log, ICAL_VTODO, ITEMLINE, _("only one location allowed.")); goto skip; } vtodo.loc = note; break; case COMMENT: /* There may be more than one. */ if (vtodo.comm) { asprintf(&comment, "%sComment: %s", vtodo.comm, note); mem_free(vtodo.comm); vtodo.comm = comment; } 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; } } ical_log(log, ICAL_VTODO, ITEMLINE, _("The ical file seems to be malformed. " "The end of item was not found.")); skip: (*noskipped)++; cleanup: if (note) mem_free(note); if (vtodo.desc) mem_free(vtodo.desc); if (vtodo.loc) 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); } /* Import calcurse data. */ void ical_import_data(const char *file, FILE * stream, FILE * log, unsigned *events, unsigned *apoints, unsigned *todos, unsigned *lines, unsigned *skipped, const char *fmt_ev, const char *fmt_rev, const char *fmt_apt, const char *fmt_rapt, const char *fmt_todo) { char buf[BUFSIZ], lstore[BUFSIZ]; int major, minor; ical_readline_init(stream, buf, lstore, lines); RETURN_IF(!ical_chk_header (stream, buf, lstore, lines, &major, &minor), _("Warning: ical header malformed or wrong version number. " "Aborting...")); ical_log_init(file, log, major, minor); while (ical_readline(stream, buf, lstore, lines)) { if (starts_with_ci(buf, "BEGIN:VEVENT")) { ical_read_event(stream, log, events, apoints, skipped, buf, lstore, lines, fmt_ev, fmt_rev, fmt_apt, fmt_rapt); } else if (starts_with_ci(buf, "BEGIN:VTODO")) { ical_read_todo(stream, log, todos, skipped, buf, lstore, lines, fmt_todo); } } } /* Export calcurse data. */ void ical_export_data(FILE * stream, int export_uid) { ical_export_header(stream); ical_export_recur_events(stream, export_uid); ical_export_events(stream, export_uid); ical_export_recur_apoints(stream, export_uid); ical_export_apoints(stream, export_uid); ical_export_todo(stream, export_uid); ical_export_footer(stream); }