From f4a9b50f8a195ffd0c9312c0fd876e4870808a18 Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Tue, 26 May 2026 17:58:30 +0200 Subject: [PATCH 1/2] Fix mishandling of exceptions in UTC Signed-off-by: Arnau Mora Gras --- .../synctools/icalendar/ICalendarGenerator.kt | 7 ++++-- .../icalendar/ICalendarGeneratorTest.kt | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/synctools/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt b/synctools/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt index 4ab142ae33..bb83e211c1 100644 --- a/synctools/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt +++ b/synctools/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt @@ -5,6 +5,7 @@ package at.bitfire.synctools.icalendar import androidx.annotation.VisibleForTesting +import at.bitfire.synctools.util.AndroidTimeUtils.toInstant import at.bitfire.synctools.util.Utils.trimToNull import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.model.Calendar @@ -13,7 +14,6 @@ import net.fortuna.ical4j.model.ComponentList import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.PropertyContainer import net.fortuna.ical4j.model.PropertyList -import net.fortuna.ical4j.model.TemporalAdapter import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.component.VTimeZone @@ -66,7 +66,10 @@ class ICalendarGenerator { ical += exception exception.dtStart()?.date?.let { start -> - if (earliestStart == null || TemporalAdapter.isBefore(start, earliestStart)) + // Convert both Temporals to Instants for comparison, because they may be in different time zones. If conversion fails, ignore the start date of this exception. + val startInstant = runCatching { start.toInstant() }.getOrNull() ?: return@let + val earliestInstant = earliestStart?.let { runCatching { it.toInstant() }.getOrNull() } + if (earliestInstant == null || startInstant < earliestInstant) earliestStart = start } usedTimezoneIds += timeZonesOf(exception) diff --git a/synctools/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt b/synctools/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt index 96d9ba294b..7673098a53 100644 --- a/synctools/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt +++ b/synctools/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt @@ -246,4 +246,27 @@ class ICalendarGeneratorTest { assertTrue(result.isEmpty()) } + @Test + fun `Write event with UTC Instant exception DTSTART does not throw`() { + val iCal = StringWriter() + writer.write(AssociatedEvents( + main = VEvent(propertyListOf( + Uid("UTCTEST"), + DtStart(ZonedDateTime.of(LocalDateTime.parse("2025-08-22T05:30:00"), tzBerlin)), + DtStamp("20250822T000000Z"), + RRule("FREQ=DAILY;COUNT=3") + )), + exceptions = listOf( + VEvent(propertyListOf( + Uid("UTCTEST"), + RecurrenceId(Instant.parse("2025-08-22T03:30:00Z")), + DtStart(Instant.parse("2025-08-22T03:30:00Z")), + DtStamp("20250822T000000Z") + )) + ), + prodId = userAgent + ), iCal) + assertTrue(iCal.toString().contains("BEGIN:VCALENDAR")) + } + } \ No newline at end of file From 5865f0ca1ead8640e98f79bc593b5455951e3c7c Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Thu, 28 May 2026 11:32:51 +0200 Subject: [PATCH 2/2] Adjust exception handling Signed-off-by: Arnau Mora Gras --- .../at/bitfire/synctools/icalendar/ICalendarGenerator.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synctools/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt b/synctools/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt index bb83e211c1..49d43c844c 100644 --- a/synctools/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt +++ b/synctools/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt @@ -66,10 +66,10 @@ class ICalendarGenerator { ical += exception exception.dtStart()?.date?.let { start -> - // Convert both Temporals to Instants for comparison, because they may be in different time zones. If conversion fails, ignore the start date of this exception. - val startInstant = runCatching { start.toInstant() }.getOrNull() ?: return@let - val earliestInstant = earliestStart?.let { runCatching { it.toInstant() }.getOrNull() } - if (earliestInstant == null || startInstant < earliestInstant) + // Convert both Temporals to Instants for comparison because they may be in different time zones. If conversion fails, UnsupportedTemporalTypeException is thrown. + val startInstant = start.toInstant() + val earliestInstant = start.toInstant() + if (startInstant < earliestInstant) earliestStart = start } usedTimezoneIds += timeZonesOf(exception)