From 382e60ba692fa8877cfa2ea531b56baa9cf2e6b7 Mon Sep 17 00:00:00 2001
From: Lars Henriksen <LarsHenriksen@get2net.dk>
Date: Thu, 28 May 2020 14:45:00 +0200
Subject: Extend import of recurrence rules

Support has been implemented for recurrence rule parts BYMONTH, BYMONTHDAY and
BYDAY.  A new test has been added.

Signed-off-by: Lars Henriksen <LarsHenriksen@get2net.dk>
Signed-off-by: Lukas Fleischer <lfleischer@calcurse.org>
---
 src/ical.c  | 213 +++++++++++++++++++++++++++++++++++++++++++++++++++++-------
 src/recur.c |   2 +-
 src/utils.c |   2 +-
 3 files changed, 192 insertions(+), 25 deletions(-)

(limited to 'src')

diff --git a/src/ical.c b/src/ical.c
index a872dd3..2946412 100644
--- a/src/ical.c
+++ b/src/ical.c
@@ -68,6 +68,9 @@ typedef struct {
 	unsigned freq;
 	time_t until;
 	unsigned count;
+	llist_t bymonth;
+	llist_t bywday;
+	llist_t bymonthday;
 } ical_rpt_t;
 
 static void ical_export_header(FILE *);
@@ -377,7 +380,7 @@ static void ical_store_todo(int priority, int completed, char *mesg,
  * 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
+static int
 ical_store_event(char *mesg, char *note, time_t day, time_t end,
 		 ical_rpt_t *irpt, llist_t *exc, const char *fmt_ev,
 		 const char *fmt_rev)
@@ -395,12 +398,14 @@ ical_store_event(char *mesg, char *note, time_t day, time_t end,
 		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.bymonth = irpt->bymonth;
+		rpt.bywday = irpt->bywday;
+		rpt.bymonthday = irpt->bymonthday;
 		rpt.exc = *exc;
-		rev = recur_event_new(mesg, note, day, EVENTID, &rpt);
+		if (!recur_item_find_occurrence(day, -1, &rpt, NULL, day, NULL))
+			return 0;
 		mem_free(irpt);
+		rev = recur_event_new(mesg, note, day, EVENTID, &rpt);
 		if (fmt_rev)
 			print_recur_event(fmt_rev, day, rev);
 		goto cleanup;
@@ -434,9 +439,10 @@ ical_store_event(char *mesg, char *note, time_t day, time_t end,
 cleanup:
 	mem_free(mesg);
 	erase_note(&note);
+	return 1;
 }
 
-static void
+static int
 ical_store_apoint(char *mesg, char *note, time_t start, long dur,
 		  ical_rpt_t * irpt, llist_t * exc, int has_alarm,
 		  const char *fmt_apt, const char *fmt_rapt)
@@ -446,6 +452,7 @@ ical_store_apoint(char *mesg, char *note, time_t start, long dur,
 	struct recur_apoint *rapt;
 	time_t day;
 
+	day = update_time_in_date(start, 0, 0);
 	if (has_alarm)
 		state |= APOINT_NOTIFY;
 	if (irpt) {
@@ -453,10 +460,13 @@ ical_store_apoint(char *mesg, char *note, time_t start, long dur,
 		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.bymonth = irpt->bymonth;
+		rpt.bywday = irpt->bywday;
+		rpt.bymonthday = irpt->bymonthday;
 		rpt.exc = *exc;
+		if (!recur_item_find_occurrence(start, dur, &rpt, NULL,
+						day, NULL))
+			return 0;
 		/*
 		 * In calcurse, "until" is interpreted as a day (DATE) - hours,
 		 * minutes and seconds are ignored - whereas in iCal the full
@@ -485,6 +495,7 @@ ical_store_apoint(char *mesg, char *note, time_t start, long dur,
 	}
 	mem_free(mesg);
 	erase_note(&note);
+	return 1;
 }
 
 /*
@@ -790,9 +801,9 @@ static void ical_count2until(time_t s, long d, ical_rpt_t *i, llist_t *e,
 	rpt.type = i->type;
 	rpt.freq = i->freq;
 	rpt.until = 0;
-	LLIST_INIT(&rpt.bymonth);
-	LLIST_INIT(&rpt.bywday);
-	LLIST_INIT(&rpt.bymonthday);
+	rpt.bymonth = i->bymonth;
+	rpt.bywday = i->bywday;
+	rpt.bymonthday = i->bymonthday;
 	recur_nth_occurrence(s, d, &rpt, e, i->count, &i->until);
 }
 
@@ -813,9 +824,108 @@ static char *ical_get_value(char *p)
 	return p + 1;
 }
 
+/*
+ * 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
@@ -855,7 +965,8 @@ static char *ical_get_value(char *p)
 static ical_rpt_t *ical_read_rrule(FILE *log, char *rrulestr,
 				   unsigned *noskipped,
 				   const int itemline,
-				   ical_vevent_e type)
+				   ical_vevent_e type,
+				   time_t start)
 {
 	char freqstr[8];
 	ical_rpt_t *rpt;
@@ -880,6 +991,9 @@ static ical_rpt_t *ical_read_rrule(FILE *log, char *rrulestr,
 
 	rpt = mem_malloc(sizeof(ical_rpt_t));
 	memset(rpt, 0, sizeof(ical_rpt_t));
+	LLIST_INIT(&rpt->bymonth);
+	LLIST_INIT(&rpt->bywday);
+	LLIST_INIT(&rpt->bymonthday);
 
 	/* FREQ rule part */
 	if ((p = strstr(rrulestr, "FREQ="))) {
@@ -960,6 +1074,42 @@ static ical_rpt_t *ical_read_rrule(FILE *log, char *rrulestr,
 		}
 	}
 
+	/* 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;
 }
 
@@ -1214,21 +1364,38 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
 				}
 				vevent.note = generate_note(string_buf(&s));
 				mem_free(s.buf);
+				/*
+				 * Necessary to prevent double-free if item
+				 * creation fails below.
+				 */
+				vevent.desc = vevent.loc = vevent.comm =
+					vevent.stat = vevent.imp = NULL;
 			}
+			char *msg = _("rrule does not match start day (%s).");
 			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);
+				if (!ical_store_apoint(vevent.mesg, vevent.note,
+						       vevent.start, vevent.dur,
+						       vevent.rpt, &vevent.exc,
+						       vevent.has_alarm,
+						       fmt_apt, fmt_rapt)) {
+					char *l = day_ins(&msg, vevent.start);
+					ical_log(log, ICAL_VEVENT, ITEMLINE, l);
+					mem_free(l);
+					goto skip;
+				}
 				(*noapoints)++;
 				break;
 			case EVENT:
-				ical_store_event(vevent.mesg, vevent.note,
-						vevent.start, vevent.end,
-						vevent.rpt, &vevent.exc,
-						fmt_ev, fmt_rev);
+				if (!ical_store_event(vevent.mesg, vevent.note,
+						      vevent.start, vevent.end,
+						      vevent.rpt, &vevent.exc,
+						      fmt_ev, fmt_rev)) {
+					char *l = day_ins(&msg, vevent.start);
+					ical_log(log, ICAL_VEVENT, ITEMLINE, l);
+					mem_free(l);
+					goto skip;
+				}
 				(*noevents)++;
 				break;
 			case UNDEFINED:
@@ -1342,7 +1509,7 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
 			}
 		} else if (starts_with_ci(buf, "RRULE")) {
 			vevent.rpt = ical_read_rrule(log, buf, noskipped,
-					ITEMLINE, vevent_type);
+					ITEMLINE, vevent_type, vevent.start);
 			if (!vevent.rpt)
 				goto cleanup;
 		} else if (starts_with_ci(buf, "EXDATE")) {
diff --git a/src/recur.c b/src/recur.c
index 29e3dd8..3c93de2 100644
--- a/src/recur.c
+++ b/src/recur.c
@@ -1828,7 +1828,7 @@ int recur_next_occurrence(time_t s, long d, struct rpt *r, llist_t *e,
 }
 
 /*
- * Finds the nth occurrence of a recurrence rule (s, d, r, e) (incl. the start)
+ * Finds the nth occurrence (incl. start)  of a recurrence rule (s, d, r, e)
  * and returns it in the provided buffer.
  */
 int recur_nth_occurrence(time_t s, long d, struct rpt *r, llist_t *e, int n,
diff --git a/src/utils.c b/src/utils.c
index 23b9d89..552c61e 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -434,9 +434,9 @@ time_t tzdate2sec(struct date day, unsigned hour, unsigned min, char *tznew)
 	tzold = getenv("TZ");
 	if (tzold)
 		tzold = mem_strdup(tzold);
-
 	setenv("TZ", tznew, 1);
 	tzset();
+
 	t = date2sec(day, hour, min);
 
 	if (tzold) {
-- 
cgit v1.2.3-70-g09d2