aboutsummaryrefslogtreecommitdiffstats
path: root/src/ical.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ical.c')
-rw-r--r--src/ical.c1591
1 files changed, 1168 insertions, 423 deletions
diff --git a/src/ical.c b/src/ical.c
index 95489a0..4a7738e 100644
--- a/src/ical.c
+++ b/src/ical.c
@@ -1,7 +1,7 @@
/*
* Calcurse - text-based organizer
*
- * Copyright (c) 2004-2017 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,
@@ -54,12 +57,13 @@ typedef enum {
EVENT
} ical_vevent_e;
-typedef struct {
- enum recur_type type;
- int freq;
- long until;
- unsigned count;
-} ical_rpt_t;
+typedef enum {
+ NO_PROPERTY,
+ SUMMARY,
+ DESCRIPTION,
+ LOCATION,
+ COMMENT
+} ical_property_e;
static void ical_export_header(FILE *);
static void ical_export_recur_events(FILE *, int);
@@ -69,8 +73,36 @@ 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[RECUR_TYPES] =
- { "", "DAILY", "WEEKLY", "MONTHLY", "YEARLY" };
+static const char *ical_recur_type[NBRECUR] =
+ { "DAILY", "WEEKLY", "MONTHLY", "YEARLY" };
+
+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(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);
+ }
+ }
+ fputc('\n', stream);
+}
/* iCal alarm notification. */
static void ical_export_valarm(FILE * stream)
@@ -83,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(&note_file, "%s/%s", path_notes, name);
+ if (!(fp = fopen(note_file, "r")) || ungetc(getc(fp), fp) == EOF) {
+ fclose(fp);
+ return;
+ }
+ string_init(&note);
+ while (fgets(lbuf, BUFSIZ, fp))
+ string_catf(&note, "%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. */
@@ -101,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:%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,
@@ -129,15 +300,9 @@ static void ical_export_recur_events(FILE * stream, int export_uid)
fputc(LLIST_NEXT(j) ? ',' : '\n', stream);
}
}
-
- fprintf(stream, "SUMMARY:%s\n", rev->mesg);
-
- if (export_uid) {
- char *hash = recur_event_hash(rev);
- fprintf(stream, "UID:%s\n", hash);
- mem_free(hash);
- }
-
+ ical_format_line(stream, "SUMMARY:", rev->mesg);
+ if (rev->note)
+ ical_export_note(stream, rev->note);
fputs("END:VEVENT\n", stream);
}
}
@@ -146,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);
- fprintf(stream, "SUMMARY:%s\n", 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);
}
}
@@ -169,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",
@@ -187,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);
}
}
-
- fprintf(stream, "SUMMARY:%s\n", rapt->mesg);
+ 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);
@@ -228,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",
@@ -244,16 +412,11 @@ static void ical_export_apoints(FILE * stream, int export_uid)
(apt->dur / MININSEC) % HOURINMIN,
apt->dur % MININSEC);
}
- fprintf(stream, "SUMMARY:%s\n", apt->mesg);
+ 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);
@@ -263,47 +426,57 @@ 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);
- fprintf(stream, "SUMMARY:%s\n", 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);
}
}
/* Print a header to describe import log report format. */
-static void ical_log_init(FILE * log, int major, int minor)
+static void ical_log_init(const char *file, FILE * log, int major, int minor)
{
const char *header =
"+-------------------------------------------------------------------+\n"
"| Calcurse icalendar import log. |\n"
"| |\n"
- "| Items imported from icalendar file, version %d.%d |\n"
- "| Some items could not be imported, they are described hereafter. |\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 represents the item type ('VEVENT' or 'VTODO') |\n"
- "| * LINE is the line in the input stream at which this item begins |\n"
- "| * DESCRIPTION indicates why the item could not be imported |\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, major, minor);
+ fprintf(log, header, file, major, minor, date);
+ mem_free(fmt);
+ mem_free(date);
}
/*
@@ -334,25 +507,37 @@ static void ical_store_todo(int priority, int completed, char *mesg,
erase_note(&note);
}
+/*
+ * 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 * rpt, 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 (!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) {
- rev = recur_event_new(mesg, note, day, EVENTID, rpt->type,
- rpt->freq, rpt->until, exc);
- mem_free(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);
@@ -360,21 +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;
- rpt = mem_malloc(sizeof(ical_rpt_t));
- rpt->type = RECUR_DAILY;
- rpt->freq = 1;
- rpt->count = 0;
- rpt->until = end;
- rev = recur_event_new(mesg, note, day, EVENTID, rpt->type,
- rpt->freq, rpt->until, exc);
- mem_free(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);
@@ -384,20 +567,41 @@ cleanup:
}
static void
-ical_store_apoint(char *mesg, char *note, long start, long dur,
- ical_rpt_t * rpt, 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 (rpt) {
- rapt = recur_apoint_new(mesg, note, start, dur, state,
- rpt->type, rpt->freq, rpt->until, exc);
- mem_free(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 {
@@ -410,10 +614,12 @@ ical_store_apoint(char *mesg, char *note, long start, long dur,
}
/*
- * Returns an allocated string representing the string given in argument once
- * unformatted.
+ * 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)
+static char *ical_unformat_line(char *line, int eol, int indentation)
{
struct string s;
char *p;
@@ -423,30 +629,37 @@ static char *ical_unformat_line(char *line)
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 't':
- string_catf(&s, "%c", '\t');
- p++;
- break;
+ case '\\':
case ';':
- case ':':
case ',':
string_catf(&s, "%c", *(p + 1));
p++;
break;
default:
- string_catf(&s, "%c", *p);
- break;
+ 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);
}
@@ -458,13 +671,13 @@ ical_readline_init(FILE * fdi, char *buf, char *lstore, unsigned *ln)
*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';
}
- (*ln)++;
}
}
@@ -473,9 +686,9 @@ static int ical_readline(FILE * fdi, char *buf, char *lstore, unsigned *ln)
char *eol;
strncpy(buf, lstore, BUFSIZ);
- (*ln)++;
while (fgets(lstore, BUFSIZ, fdi) != NULL) {
+ (*ln)++;
if ((eol = strchr(lstore, '\n')) != NULL) {
if (*(eol - 1) == '\r')
*(eol - 1) = '\0';
@@ -485,7 +698,6 @@ static int ical_readline(FILE * fdi, char *buf, char *lstore, unsigned *ln)
if (*lstore != SPACE && *lstore != TAB)
break;
strncat(buf, lstore + 1, BUFSIZ - strlen(buf) - 1);
- (*ln)++;
}
if (feof(fdi)) {
@@ -516,39 +728,83 @@ 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)
+{
+ 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).
*
- * Optionally, if the type pointer is given, specify if it is an event
- * (no time is given, meaning it is an all-day event), or an appointment
- * (time is given).
- *
- * 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 FORMAT_DATE = 3, FORMAT_DATETIME = 6, FORMAT_DATETIMEZ = 7;
+ 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 == FORMAT_DATE) {
- if (type)
- *type = EVENT;
+
+ 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 == FORMAT_DATETIME || format == FORMAT_DATETIMEZ) {
- if (type)
- *type = APPOINTMENT;
- if (format == FORMAT_DATETIMEZ && c == 'Z')
- return utcdate2sec(date, hour, min);
- else
- return date2sec(date, hour, min);
- }
+ else if (format == DATETIME && type == APPOINTMENT)
+ return tzdate2sec(date, hour, min, tzid);
+ else if (format == DATETIMEZ && type == APPOINTMENT)
+ return tzdate2sec(date, hour, min, UTC);
+
return 0;
}
@@ -582,15 +838,22 @@ static long ical_durtime2long(char *timestr)
}
/*
- * Extract from RFC2445:
+ * 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
- * duration of time.
+ * a duration of time.
*
- * Formal Definition: The value type is defined by the following
- * notation:
+ * 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]
@@ -601,73 +864,41 @@ 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:
* P7W
*/
-static long ical_dur2long(char *durstr)
+static long ical_dur2long(char *durstr, ical_vevent_e type)
{
- char *p;
+ char *p = durstr, c;
int bytes_read;
- struct {
- unsigned week, day;
- } date;
-
- memset(&date, 0, sizeof date);
-
- p = strchr(durstr, 'P');
- if (!p)
- return -1;
- p++;
+ unsigned week, day;
if (*p == '-')
- return -1;
+ return 0;
if (*p == '+')
p++;
+ if (*p != 'P')
+ return 0;
- if (*p == 'T') {
+ p++;
+ if (*p == 'T' && type == APPOINTMENT)
/* dur-time */
return ical_durtime2long(p);
- } else if (strchr(p, 'W')) {
+ else if (sscanf(p, "%u%c", &week, &c) == 2 && c == 'W')
/* dur-week */
- if (sscanf(p, "%u", &date.week) == 1)
- return date.week * WEEKINDAYS * DAYINSEC;
- } else if (strchr(p, 'D')) {
+ return week * WEEKINDAYS * DAYINSEC;
+ else if (sscanf(p, "%u%c%n", &day, &c, &bytes_read) == 2 && c == 'D') {
/* dur-date */
- if (sscanf(p, "%uD%n", &date.day, &bytes_read) == 1) {
- p += bytes_read;
- return date.day * DAYINSEC + ical_durtime2long(p);
- }
+ p += bytes_read;
+ return day * DAYINSEC + (*p == 'T' && type == APPOINTMENT ?
+ ical_durtime2long(p) :
+ 0);
}
- return -1;
-}
-
-/*
- * 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;
- }
+ return 0;
}
/*
@@ -675,10 +906,12 @@ static long ical_compute_rpt_until(long start, ical_rpt_t * rpt)
*/
static char *ical_get_value(char *p)
{
+ if (!(p && *p))
+ return NULL;
for (; *p != ':'; p++) {
if (*p == '"')
- for (p++; *p != '"' && *p != '\0'; p++);
- if (*p == '\0')
+ for (p++; *p && *p != '"'; p++);
+ if (!*p)
return NULL;
}
@@ -686,125 +919,304 @@ 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 *(
- *
- * ; 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 ) /
+ * 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.
*
- * ; 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)
+ * 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,
+ 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;
+
+ 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,
- _("recurrence rule malformed."));
+ _("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) {
+ /* 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);
+
+ /* 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,
- _("recurrence frequency not recognized."));
+ _("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,
+ _("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, 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;
+ if ((p = strstr(rrulestr, "COUNT="))) {
+ p = strchr(p, '=') + 1;
+ if (!(sscanf(p, "%d", count) == 1 && *count)) {
+ ical_log(log, ICAL_VEVENT, itemline,
+ _("invalid count value."));
+ (*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;
+ /* 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;
+ }
+ }
+
+ /* 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;
}
-static void ical_add_exc(llist_t * exc_head, long date)
+static void ical_add_exc(llist_t * exc_head, time_t date)
{
- if (date == 0)
- return;
-
struct excp *exc = mem_malloc(sizeof(struct excp));
exc->st = date;
@@ -812,83 +1224,125 @@ static void ical_add_exc(llist_t * exc_head, long date)
}
/*
- * This property defines the list of date/time exceptions for a
+ * This property defines a comma-separated list of date/time exceptions for a
* recurring calendar component.
*/
-static void
-ical_read_exdate(llist_t * exc, FILE * log, char *exstr,
- unsigned *noskipped, const int itemline)
+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;
+ if (type != ical_get_type(exstr)) {
+ ical_log(log, ICAL_VEVENT, itemline,
+ _("invalid exception date value type."));
+ goto cleanup;
+ }
p = ical_get_value(exstr);
if (!p) {
ical_log(log, ICAL_VEVENT, itemline,
- _("recurrence exception dates malformed."));
- (*noskipped)++;
- return;
+ _("malformed exceptions line."));
+ goto cleanup;
}
-
- while ((q = strchr(p, ',')) != NULL) {
- char buf[BUFSIZ];
- const int buflen = q - p;
-
- strncpy(buf, p, buflen);
- buf[buflen] = '\0';
- ical_add_exc(exc, ical_datetime2time_t(buf, NULL));
- p = ++q;
+ 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, tzid, type))) {
+ ical_log(log, ICAL_VEVENT, itemline,
+ _("invalid exception."));
+ goto cleanup;
+ }
+ ical_add_exc(exc, t);
+ p = strchr(p, '\0') + 1;
+ n--;
}
- ical_add_exc(exc, ical_datetime2time_t(p, NULL));
+ return 1;
+
+cleanup:
+ (*noskipped)++;
+ if (tzid)
+ mem_free(tzid);
+ return 0;
}
-/* Return an allocated string containing the name of the newly created note. */
-static char *ical_read_note(char *line, unsigned *noskipped,
+/*
+ * 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)
{
- char *p, *notestr, *note;
+ const int EOL = 1,
+ IND = (property != DESCRIPTION);
+ char *p, *pname, *notestr;
+
+ switch (property) {
+ case DESCRIPTION:
+ pname = "description";
+ break;
+ case LOCATION:
+ pname = "location";
+ break;
+ case COMMENT:
+ pname = "comment";
+ break;
+ default:
+ pname = "no property";
+ }
p = ical_get_value(line);
if (!p) {
- ical_log(log, item_type, itemline,
- _("description malformed."));
+ asprintf(&p, _("malformed %s line."), pname);
+ ical_log(log, item_type, itemline, p);
+ mem_free(p);
(*noskipped)++;
- return NULL;
+ notestr = NULL;
+ goto leave;
}
- notestr = ical_unformat_line(p);
- if (notestr == NULL) {
- ical_log(log, item_type, itemline,
- _("could not get entire item description."));
+ notestr = ical_unformat_line(p, EOL, IND);
+ if (!notestr) {
+ asprintf(&p, _("malformed %s."), pname);
+ ical_log(log, item_type, itemline, p);
+ mem_free(p);
(*noskipped)++;
- return NULL;
- } else if (strlen(notestr) == 0) {
- mem_free(notestr);
- return NULL;
- } else {
- note = generate_note(notestr);
- mem_free(notestr);
- return note;
}
+ leave:
+ return notestr;
}
/* Returns an allocated string containing the ical item summary. */
-static char *ical_read_summary(char *line)
+static char *ical_read_summary(char *line, unsigned *noskipped,
+ ical_types_e item_type, const int itemline,
+ FILE * log)
{
- char *p, *summary;
+ const int EOL = 0, IND = 0;
+ char *p, *summary = NULL;
p = ical_get_value(line);
- if (!p)
- return NULL;
-
- summary = ical_unformat_line(p);
- if (!summary)
- return NULL;
+ if (!p) {
+ ical_log(log, item_type, itemline, _("malformed summary line."));
+ (*noskipped)++;
+ goto leave;
+ }
- /* Event summaries must not contain newlines. */
- for (p = strchr(summary, '\n'); p; p = strchr(p, '\n'))
- *p = ' ';
+ summary = ical_unformat_line(p, EOL, IND);
+ if (!summary) {
+ ical_log(log, item_type, itemline, _("malformed summary."));
+ (*noskipped)++;
+ goto leave;
+ }
+ /* An event summary is one line only. */
+ for (p = summary; *p; p++)
+ if (*p == '\n')
+ *p = ' ';
+ leave:
return summary;
}
@@ -898,23 +1352,31 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
char *lstore, unsigned *lineno, const char *fmt_ev,
const char *fmt_rev, const char *fmt_apt, const char *fmt_rapt)
{
- const int ITEMLINE = *lineno;
+ const int ITEMLINE = *lineno - !feof(fdi);
ical_vevent_e vevent_type;
- char *p;
+ ical_property_e property;
+ char *p, *note, *tzid;
+ char *dtstart, *dtend, *duration, *rrule;
+ struct string s, exdate;
struct {
llist_t exc;
- ical_rpt_t *rpt;
- char *mesg, *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;
+ int skip_alarm, has_note, separator, has_exdate;
vevent_type = UNDEFINED;
memset(&vevent, 0, sizeof vevent);
LLIST_INIT(&vevent.exc);
- skip_alarm = 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;
if (skip_alarm) {
/*
* Need to skip VALARM properties because some keywords
@@ -924,145 +1386,355 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
skip_alarm = 0;
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."));
- goto cleanup;
+ _("item start date not defined."));
+ goto skip;
+ }
+ 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,
+ _("malformed start time line."));
+ 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;
}
- if (vevent.start == 0) {
+ /* DTEND */
+ if (!dtend)
+ goto duration;
+ if (vevent_type != ical_get_type(dtend)) {
ical_log(log, ICAL_VEVENT, ITEMLINE,
- _("item start date is not defined."));
+ _("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;
+ }
+ 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 (vevent_type == APPOINTMENT && vevent.dur == 0) {
- if (vevent.end != 0) {
- vevent.dur = vevent.end - vevent.start;
+ 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 (vevent.dur < 0) {
- ical_log(log, ICAL_VEVENT, ITEMLINE,
- _("item has a negative duration."));
- goto cleanup;
+ }
+ 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);
+ 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.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 && vevent.rpt->count) {
- vevent.rpt->until =
- ical_compute_rpt_until(vevent.start,
- vevent.rpt);
+ 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:
ical_log(log, ICAL_VEVENT, ITEMLINE,
_("item could not be identified."));
- goto cleanup;
+ goto skip;
break;
}
-
return;
}
-
if (starts_with_ci(buf, "DTSTART")) {
- p = ical_get_value(buf);
- if (!p) {
- ical_log(log, ICAL_VEVENT, ITEMLINE,
- _("event start time malformed."));
- goto cleanup;
- }
-
- vevent.start = ical_datetime2time_t(p, &vevent_type);
- if (!vevent.start) {
- ical_log(log, ICAL_VEVENT, ITEMLINE,
- _("could not retrieve event start time."));
- goto cleanup;
- }
+ /*
+ * 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.
+ */
+ asprintf(&dtstart, "%s", buf);
} else if (starts_with_ci(buf, "DTEND")) {
- p = ical_get_value(buf);
- if (!p) {
- ical_log(log, ICAL_VEVENT, ITEMLINE,
- _("event end time malformed."));
- goto cleanup;
- }
-
- vevent.end = ical_datetime2time_t(p, &vevent_type);
- if (!vevent.end) {
- ical_log(log, ICAL_VEVENT, ITEMLINE,
- _("could not retrieve event end time."));
- goto cleanup;
- }
+ asprintf(&dtend, "%s", buf);
} else if (starts_with_ci(buf, "DURATION")) {
- vevent.dur = ical_dur2long(buf);
- if (vevent.dur <= 0) {
- ical_log(log, ICAL_VEVENT, ITEMLINE,
- _("item duration malformed."));
- goto cleanup;
- }
+ asprintf(&duration, "%s", buf);
} else if (starts_with_ci(buf, "RRULE")) {
- vevent.rpt = ical_read_rrule(log, buf, noskipped,
- ITEMLINE);
+ asprintf(&rrule, "%s", buf);
} else if (starts_with_ci(buf, "EXDATE")) {
- ical_read_exdate(&vevent.exc, log, buf, noskipped,
- ITEMLINE);
+ 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);
+ 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")) {
- vevent.note = ical_read_note(buf, noskipped,
- ICAL_VEVENT, ITEMLINE, log);
+ property = DESCRIPTION;
+ } else if (starts_with_ci(buf, "LOCATION")) {
+ property = LOCATION;
+ } else if (starts_with_ci(buf, "COMMENT")) {
+ property = COMMENT;
+ }
+ if (property) {
+ note = ical_read_note(buf, property, noskipped,
+ ICAL_VEVENT, ITEMLINE, log);
+ if (!note)
+ goto cleanup;
+ if (!separator)
+ 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(&p, "%sComment: %s",
+ vevent.comm, note);
+ mem_free(vevent.comm);
+ vevent.comm = p;
+ } else
+ vevent.comm = 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 (vevent.note)
- mem_free(vevent.note);
+ 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)
+ mem_free(vevent.desc);
+ if (vevent.loc)
+ mem_free(vevent.loc);
+ if (vevent.comm)
+ mem_free(vevent.comm);
+ if (vevent.imp)
+ mem_free(vevent.imp);
if (vevent.mesg)
mem_free(vevent.mesg);
if (vevent.rpt)
mem_free(vevent.rpt);
LLIST_FREE(&vevent.exc);
- (*noskipped)++;
}
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;
+ const int ITEMLINE = *lineno - !feof(fdi);
+ ical_property_e property;
+ char *p, *note;
+ struct string s;
struct {
- char *mesg, *note;
+ char *mesg, *desc, *loc, *comm, *note;
int priority;
int completed;
} vtodo;
- int skip_alarm;
+ int skip_alarm, has_note, separator;
memset(&vtodo, 0, sizeof vtodo);
- skip_alarm = 0;
+ note = NULL;
+ 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
@@ -1072,54 +1744,128 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
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);
+ 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);
+ vtodo.comm = NULL;
+ }
+ 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;
} else if (starts_with_ci(buf, "SUMMARY")) {
- vtodo.mesg = ical_read_summary(buf);
+ 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")) {
- vtodo.note = ical_read_note(buf, noskipped, ICAL_VTODO,
- ITEMLINE, log);
+ property = DESCRIPTION;
+ } else if (starts_with_ci(buf, "LOCATION")) {
+ property = LOCATION;
+ } else if (starts_with_ci(buf, "COMMENT")) {
+ property = COMMENT;
+ }
+ if (property) {
+ note = ical_read_note(buf, property, noskipped,
+ ICAL_VTODO, ITEMLINE, log);
+ if (!note)
+ goto cleanup;
+ if (!separator)
+ 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(&p, "%sComment: %s",
+ vtodo.comm, note);
+ mem_free(vtodo.comm);
+ vtodo.comm = p;
+ } else
+ vtodo.comm = 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 (vtodo.note)
- mem_free(vtodo.note);
+ 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.mesg)
mem_free(vtodo.mesg);
- (*noskipped)++;
}
/* Import calcurse data. */
void
-ical_import_data(FILE * stream, FILE * log, unsigned *events,
+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,
@@ -1134,10 +1880,9 @@ ical_import_data(FILE * stream, FILE * log, unsigned *events,
_("Warning: ical header malformed or wrong version number. "
"Aborting..."));
- ical_log_init(log, major, minor);
+ ical_log_init(file, log, major, minor);
while (ical_readline(stream, buf, lstore, lines)) {
- (*lines)++;
if (starts_with_ci(buf, "BEGIN:VEVENT")) {
ical_read_event(stream, log, events, apoints,
skipped, buf, lstore, lines, fmt_ev,