fix(ics): reject an RRULE carrying both UNTIL and COUNT (#225)
EventRecurrence.parse ends with two global checks, not one: a missing FREQ throws, and so does UNTIL together with COUNT. Only the first was mirrored, so a rule with both still threw out of insert — the failure sanitizeRrule exists to prevent. UNTIL is kept; it bounds the series at a date a stale COUNT can't outrun. Also mark a part that carries text but no '=' as a repair. It took the branch meant for a trailing ';', so the rule was altered while the user was told nothing had changed.
This commit is contained in:
@@ -80,10 +80,12 @@ fun sanitizeRrule(raw: String?): SanitizedRrule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (part in rule.split(';')) {
|
for (part in rule.split(';')) {
|
||||||
|
// A stray separator ("FREQ=DAILY;") normalises away; nothing was lost.
|
||||||
|
// A part that carries text but no '=' ("FREQ=WEEKLY;BYDAY") is a part
|
||||||
|
// being dropped, so it falls through and marks the rule repaired.
|
||||||
|
if (part.isBlank()) continue
|
||||||
val key = part.substringBefore('=', "").trim().uppercase()
|
val key = part.substringBefore('=', "").trim().uppercase()
|
||||||
val value = if ('=' in part) part.substringAfter('=').trim() else ""
|
val value = if ('=' in part) part.substringAfter('=').trim() else ""
|
||||||
// A stray separator ("FREQ=DAILY;") normalises away; nothing was lost.
|
|
||||||
if (key.isEmpty() && value.isEmpty()) continue
|
|
||||||
val kept = when {
|
val kept = when {
|
||||||
key.isEmpty() || value.isEmpty() -> null
|
key.isEmpty() || value.isEmpty() -> null
|
||||||
key == "FREQ" ->
|
key == "FREQ" ->
|
||||||
@@ -105,6 +107,15 @@ fun sanitizeRrule(raw: String?): SanitizedRrule {
|
|||||||
}
|
}
|
||||||
if (kept == null) repaired = true else parts += kept
|
if (kept == null) repaired = true else parts += kept
|
||||||
}
|
}
|
||||||
|
// `EventRecurrence.parse` ends with *two* global checks, not one: a missing
|
||||||
|
// FREQ throws, and so does an UNTIL that arrives together with a COUNT. A
|
||||||
|
// rule carrying both is malformed per RFC 5545 §3.3.10 but real producers
|
||||||
|
// emit it, and left alone it would throw straight out of `insert` — the very
|
||||||
|
// failure this function exists to prevent. UNTIL is kept: it bounds the
|
||||||
|
// series at an absolute date, so a stale COUNT can't over-generate past it.
|
||||||
|
if (parts.any { it.startsWith("UNTIL=") }) {
|
||||||
|
if (parts.removeAll { it.startsWith("COUNT=") }) repaired = true
|
||||||
|
}
|
||||||
return if (freqSeen) {
|
return if (freqSeen) {
|
||||||
SanitizedRrule(rule = parts.joinToString(";"), repaired = repaired)
|
SanitizedRrule(rule = parts.joinToString(";"), repaired = repaired)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -129,4 +129,38 @@ class IcsRecurrenceTest {
|
|||||||
.isEqualTo("FREQ=DAILY;UNTIL=20260819T235959Z")
|
.isEqualTo("FREQ=DAILY;UNTIL=20260819T235959Z")
|
||||||
assertThat(sanitizeRrule("FREQ=DAILY;UNTIL=2026-08-19").rule).isEqualTo("FREQ=DAILY")
|
assertThat(sanitizeRrule("FREQ=DAILY;UNTIL=2026-08-19").rule).isEqualTo("FREQ=DAILY")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `UNTIL and COUNT together keep UNTIL`() {
|
||||||
|
// EventRecurrence.parse throws outright on a rule carrying both.
|
||||||
|
val both = sanitizeRrule("FREQ=WEEKLY;COUNT=10;UNTIL=20260101T000000Z")
|
||||||
|
|
||||||
|
assertThat(both.rule).isEqualTo("FREQ=WEEKLY;UNTIL=20260101T000000Z")
|
||||||
|
assertThat(both.repaired).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a COUNT the UNTIL check never sees is left alone`() {
|
||||||
|
// An UNTIL dropped for its shape doesn't take the COUNT down with it.
|
||||||
|
val staleUntil = sanitizeRrule("FREQ=WEEKLY;COUNT=10;UNTIL=2026-01-01")
|
||||||
|
|
||||||
|
assertThat(staleUntil.rule).isEqualTo("FREQ=WEEKLY;COUNT=10")
|
||||||
|
assertThat(staleUntil.repaired).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a part without a value separator is reported as repaired`() {
|
||||||
|
val noEquals = sanitizeRrule("FREQ=WEEKLY;BYDAY")
|
||||||
|
|
||||||
|
assertThat(noEquals.rule).isEqualTo("FREQ=WEEKLY")
|
||||||
|
assertThat(noEquals.repaired).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a trailing separator is normalisation, not a repair`() {
|
||||||
|
val trailing = sanitizeRrule("FREQ=DAILY;")
|
||||||
|
|
||||||
|
assertThat(trailing.rule).isEqualTo("FREQ=DAILY")
|
||||||
|
assertThat(trailing.repaired).isFalse()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user