Skip to content

Commit 1fd1eaf

Browse files
authored
Ignore recurrence rules whose UNTIL is on or before DTSTART (#95)
* Ignore recurrence rules whose UNTIL is on or before DTSTART When parsing RRULE and EXRULE, skip rules whose UNTIL ≤ event DTSTART and log a warning. Add unit tests for RRULE/EXRULE with UNTIL before DTSTART and rename the test class. * Add test verifying calendar provider insertion of event with RRULE whose UNTIL is before DTSTART * Remove check from EventValidator * Remove obsolete tests * Fix text * Support case when the only RRULE is filtered, so that the EXDATE should be dropped too + test * Don't generate exception VEVENTs when there are only invalid/filtered recurrence properties, so that the generated event is non-recurring * Split time builder into start and end time builders (#102) * [WIP] Split TimeFieldsBuilder into StartTimeBuilder and EndTimeBuilder; make operation more clear * [WIP] Continue with EndTimeBuilder; drop TimeFieldsBuilder * [WIP] Naming changes * Finish EndTimeBuilder * Add DurationBuilder and update EndTimeBuilder for recurring events - Add DurationBuilder to handle duration for recurring events - Update EndTimeBuilder to skip DTEND for recurring events - Add unit tests for both builders - Fix Duration.toRfc5545Duration() for zero seconds * Refactor DurationBuilder to remove debug print and use Duration.ZERO - Remove the `System.err.println` statement that prints `startDate` and `endDate`. - Replace `java.time.Duration.ofSeconds(0)` with `java.time.Duration.ZERO` * Add / refactor tests
1 parent 9529acf commit 1fd1eaf

30 files changed

+1037
-902
lines changed

lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventBuilder2Test.kt

Lines changed: 0 additions & 487 deletions
This file was deleted.

lib/src/androidTest/kotlin/at/bitfire/synctools/storage/calendar/AndroidCalendarProviderBehaviorTest.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import androidx.test.platform.app.InstrumentationRegistry
1818
import androidx.test.rule.GrantPermissionRule
1919
import at.bitfire.ical4android.impl.TestCalendar
2020
import at.bitfire.ical4android.util.MiscUtils.closeCompat
21+
import at.bitfire.synctools.test.assertContentValuesEqual
2122
import org.junit.After
2223
import org.junit.Before
2324
import org.junit.Rule
@@ -55,6 +56,39 @@ class AndroidCalendarProviderBehaviorTest {
5556
}
5657

5758

59+
@Test
60+
fun testInsertEventWithDurationZeroSeconds() {
61+
// To make sure that it's not a problem to insert a recurring event with a duration of zero seconds.
62+
val values = contentValuesOf(
63+
Events.CALENDAR_ID to calendar.id,
64+
Events.DTSTART to 1759403653000, // Thu Oct 02 2025 11:14:13 GMT+0000
65+
Events.DURATION to "PT0S",
66+
Events.TITLE to "Event with useless RRULE",
67+
Events.RRULE to "FREQ=DAILY;UNTIL=20251002T000000Z"
68+
)
69+
val id = calendar.addEvent(Entity(values))
70+
71+
val event2 = calendar.getEventRow(id)
72+
assertContentValuesEqual(values, event2!!, onlyFieldsInExpected = true)
73+
}
74+
75+
@Test
76+
fun testInsertEventWithRRuleUntilBeforeDtStart() {
77+
// To make sure that's not a problem to insert an (invalid/useless) RRULE with UNTIL before the event's DTSTART.
78+
val values = contentValuesOf(
79+
Events.CALENDAR_ID to calendar.id,
80+
Events.DTSTART to 1759403653000, // Thu Oct 02 2025 11:14:13 GMT+0000
81+
Events.DURATION to "PT1H",
82+
Events.TITLE to "Event with useless RRULE",
83+
Events.RRULE to "FREQ=DAILY;UNTIL=20251002T000000Z"
84+
)
85+
val id = calendar.addEvent(Entity(values))
86+
87+
val event2 = calendar.getEventRow(id)
88+
assertContentValuesEqual(values, event2!!, onlyFieldsInExpected = true)
89+
}
90+
91+
5892
/**
5993
* Reported as https://issuetracker.google.com/issues/446730408.
6094
*/

lib/src/main/kotlin/at/bitfire/ical4android/Event.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package at.bitfire.ical4android
88

9+
import at.bitfire.synctools.exception.InvalidICalendarException
910
import at.bitfire.synctools.icalendar.Css3Color
1011
import net.fortuna.ical4j.model.Parameter
1112
import net.fortuna.ical4j.model.Property
@@ -91,4 +92,7 @@ data class Event(
9192
return email
9293
}
9394

95+
fun requireDtStart(): DtStart =
96+
dtStart ?: throw InvalidICalendarException("Missing DTSTART in VEVENT")
97+
9498
}

lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ package at.bitfire.ical4android
88

99
import at.bitfire.ical4android.ICalendar.Companion.CALENDAR_NAME
1010
import at.bitfire.synctools.BuildConfig
11-
import at.bitfire.synctools.exception.InvalidRemoteResourceException
11+
import at.bitfire.synctools.exception.InvalidICalendarException
1212
import at.bitfire.synctools.icalendar.ICalendarParser
1313
import at.bitfire.synctools.icalendar.validation.ICalPreprocessor
1414
import net.fortuna.ical4j.data.CalendarBuilder
@@ -89,7 +89,7 @@ open class ICalendar {
8989
*
9090
* @return parsed iCalendar resource
9191
*
92-
* @throws InvalidRemoteResourceException when the iCalendar can't be parsed
92+
* @throws InvalidICalendarException when the iCalendar can't be parsed
9393
*/
9494
@Deprecated("Use ICalendarParser directly")
9595
fun fromReader(

lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import android.os.ParcelFileDescriptor
1414
import android.util.Base64
1515
import androidx.core.content.contentValuesOf
1616
import at.bitfire.ical4android.ICalendar.Companion.withUserAgents
17-
import at.bitfire.synctools.exception.InvalidRemoteResourceException
17+
import at.bitfire.synctools.exception.InvalidICalendarException
1818
import at.bitfire.synctools.icalendar.Css3Color
1919
import at.bitfire.synctools.storage.BatchOperation
2020
import at.bitfire.synctools.storage.JtxBatchOperation
@@ -285,7 +285,7 @@ open class JtxICalObject(
285285
*
286286
* @return array of filled [JtxICalObject] data objects (may have size 0)
287287
*
288-
* @throws InvalidRemoteResourceException when the iCalendar can't be parsed
288+
* @throws InvalidICalendarException when the iCalendar can't be parsed
289289
* @throws IOException on I/O errors
290290
*/
291291
fun fromReader(

lib/src/main/kotlin/at/bitfire/ical4android/Task.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ package at.bitfire.ical4android
88

99
import androidx.annotation.IntRange
1010
import at.bitfire.ical4android.util.DateUtils
11-
import at.bitfire.synctools.exception.InvalidRemoteResourceException
11+
import at.bitfire.synctools.exception.InvalidICalendarException
1212
import at.bitfire.synctools.icalendar.Css3Color
1313
import net.fortuna.ical4j.data.CalendarOutputter
1414
import net.fortuna.ical4j.model.Calendar
@@ -107,7 +107,7 @@ data class Task(
107107
*
108108
* @return array of filled [Task] data objects (may have size 0)
109109
*
110-
* @throws InvalidRemoteResourceException when the iCalendar can't be parsed
110+
* @throws InvalidICalendarException when the iCalendar can't be parsed
111111
* @throws IOException on I/O errors
112112
*/
113113
fun tasksFromReader(reader: Reader): List<Task> {

lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ object AndroidTimeUtils {
4242
/**
4343
* Timezone ID to store for all-day events, according to CalendarContract.Events SDK documentation.
4444
*/
45-
val TZID_ALLDAY = "UTC"
45+
val TZID_UTC = "UTC"
4646

4747
private const val RECURRENCE_LIST_TZID_SEPARATOR = ';'
4848
private const val RECURRENCE_LIST_VALUE_SEPARATOR = ","
@@ -136,7 +136,7 @@ object AndroidTimeUtils {
136136
}
137137
} else
138138
// DATE
139-
TZID_ALLDAY
139+
TZID_UTC
140140

141141

142142
// recurrence sets

lib/src/main/kotlin/at/bitfire/ical4android/util/TimeApiExtensions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ object TimeApiExtensions {
152152
var secs = seconds
153153

154154
if (secs == 0L)
155-
return "P0S"
155+
return "PT0S"
156156

157157
var weeks = secs / SECONDS_PER_WEEK
158158
secs -= weeks * SECONDS_PER_WEEK

lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ object EventValidator {
4747
fun repair(event: Event) {
4848
val dtStart = correctStartAndEndTime(event)
4949
sameTypeForDtStartAndRruleUntil(dtStart, event.rRules)
50-
removeRRulesWithUntilBeforeDtStart(dtStart, event.rRules)
5150
}
5251

5352

@@ -167,28 +166,4 @@ object EventValidator {
167166
}
168167
}
169168

170-
/**
171-
* Will remove the RRULES of an event where UNTIL lies before DTSTART
172-
*/
173-
@VisibleForTesting
174-
internal fun removeRRulesWithUntilBeforeDtStart(dtStart: DtStart, rRules: MutableList<RRule>) {
175-
val iter = rRules.iterator()
176-
while (iter.hasNext()) {
177-
val rRule = iter.next()
178-
179-
// drop invalid RRULEs
180-
if (hasUntilBeforeDtStart(dtStart, rRule))
181-
iter.remove()
182-
}
183-
}
184-
185-
/**
186-
* Checks whether UNTIL of an RRULE lies before DTSTART
187-
*/
188-
@VisibleForTesting
189-
internal fun hasUntilBeforeDtStart(dtStart: DtStart, rRule: RRule): Boolean {
190-
val until = rRule.recur.until ?: return false
191-
return until < dtStart.date
192-
}
193-
194169
}

lib/src/main/kotlin/at/bitfire/synctools/exception/InvalidRemoteResourceException.kt renamed to lib/src/main/kotlin/at/bitfire/synctools/exception/InvalidICalendarException.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
package at.bitfire.synctools.exception
88

99
/**
10-
* Represents an invalid remote resource (for instance, a calendar object resource).
10+
* Represents an invalid iCalendar resource.
1111
*/
12-
class InvalidRemoteResourceException: InvalidResourceException {
12+
class InvalidICalendarException: InvalidResourceException {
1313

1414
constructor(message: String): super(message)
1515
constructor(message: String, ex: Throwable): super(message, ex)

0 commit comments

Comments
 (0)