Skip to content

Commit b804030

Browse files
authoredApr 18, 2024··
Avoid infinite loop with invalid YEARLY recurrence rule (#621)
Produce iterator with no occurrences for impossible YEARLY recurrence rule.
1 parent ae06bf4 commit b804030

File tree

2 files changed

+59
-6
lines changed

2 files changed

+59
-6
lines changed
 

‎lib/ical/recur_iterator.js

+44-6
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,19 @@ class RecurIterator {
179179
this.initialized = options.initialized || false;
180180

181181
if (!this.initialized) {
182-
this.init();
182+
try {
183+
this.init();
184+
} catch (e) {
185+
if (e instanceof InvalidRecurrenceRuleError) {
186+
// Init may error if there are no possible recurrence instances from
187+
// the rule, but we don't want to bubble this error up. Instead, we
188+
// create an empty iterator.
189+
this.completed = true;
190+
} else {
191+
// Propagate other errors to consumers.
192+
throw e;
193+
}
194+
}
183195
}
184196
}
185197

@@ -251,14 +263,28 @@ class RecurIterator {
251263
}
252264

253265
if (this.rule.freq == "YEARLY") {
254-
for (;;) {
266+
// Some yearly recurrence rules may be specific enough to not actually
267+
// occur on a yearly basis, e.g. the 29th day of February or the fifth
268+
// Monday of a given month. The standard isn't clear on the intended
269+
// behavior in these cases, but `libical` at least will iterate until it
270+
// finds a matching year.
271+
// CAREFUL: Some rules may specify an occurrence that can never happen,
272+
// e.g. the first Monday of April so long as it falls on the 15th
273+
// through the 21st. Detecting these is non-trivial, so ensure that we
274+
// stop iterating at some point.
275+
const untilYear = this.rule.until ? this.rule.until.year : 20000;
276+
while (this.last.year <= untilYear) {
255277
this.expand_year_days(this.last.year);
256278
if (this.days.length > 0) {
257279
break;
258280
}
259281
this.increment_year(this.rule.interval);
260282
}
261283

284+
if (this.days.length == 0) {
285+
throw new InvalidRecurrenceRuleError();
286+
}
287+
262288
this._nextByYearDay();
263289
}
264290

@@ -348,11 +374,10 @@ class RecurIterator {
348374

349375
if ((this.rule.count && this.occurrence_number >= this.rule.count) ||
350376
(this.rule.until && this.last.compare(this.rule.until) > 0)) {
351-
352-
//XXX: right now this is just a flag and has no impact
353-
// we can simplify the above case to check for completed later.
354377
this.completed = true;
378+
}
355379

380+
if (this.completed) {
356381
return null;
357382
}
358383

@@ -362,7 +387,6 @@ class RecurIterator {
362387
return this.last;
363388
}
364389

365-
366390
let valid;
367391
do {
368392
valid = 1;
@@ -1391,4 +1415,18 @@ class RecurIterator {
13911415
return result;
13921416
}
13931417
}
1418+
1419+
/**
1420+
* An error indicating that a recurrence rule is invalid and produces no
1421+
* occurrences.
1422+
*
1423+
* @extends {Error}
1424+
* @class
1425+
*/
1426+
class InvalidRecurrenceRuleError extends Error {
1427+
constructor() {
1428+
super("Recurrence rule has no valid occurrences");
1429+
}
1430+
}
1431+
13941432
export default RecurIterator;

‎test/recur_iterator_test.js

+15
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,22 @@ suite('recur_iterator', function() {
143143

144144
let start = ICAL.Time.fromString(options.dtStart);
145145
let recur = ICAL.Recur.fromString(ruleString);
146+
146147
if (options.throws) {
147148
assert.throws(function() {
148149
recur.iterator(start);
149150
});
150151
return;
151152
}
153+
152154
let iterator = recur.iterator(start);
153155

156+
if (options.noInstance) {
157+
assert.equal(iterator.next(), null);
158+
assert.ok(iterator.completed);
159+
return;
160+
}
161+
154162
let inc = 0;
155163
let dates = [];
156164
let next, max;
@@ -1081,6 +1089,13 @@ suite('recur_iterator', function() {
10811089
]
10821090
});
10831091

1092+
// Invalid recurrence rule. The first Monday can never fall later than the
1093+
// 7th.
1094+
testRRULE('FREQ=YEARLY;BYMONTHDAY=15,16,17,18,19,20,21;BYDAY=1MO', {
1095+
dtStart: '2015-01-01T08:00:00',
1096+
noInstance: true,
1097+
});
1098+
10841099
// Tycho brahe days - yearly, byYearDay with negative offsets
10851100
testRRULE('FREQ=YEARLY;BYYEARDAY=1,2,4,6,11,12,20,42,48,49,-306,-303,' +
10861101
'-293,-292,-266,-259,-258,-239,-228,-209,-168,-164,-134,-133,' +

0 commit comments

Comments
 (0)
Please sign in to comment.