bitfireAT / ical4android

Allows usage of iCalendar files with the Android calendar provider
GNU General Public License v3.0
19 stars 10 forks source link

"UnsupportedOperationException: TimeZone is not applicable to current value" when parsing iCalendar from iCloud #110

Closed rfc2822 closed 1 year ago

rfc2822 commented 1 year ago

We got this parsing error over a support request:

EXCEPTION
at.bitfire.ical4android.InvalidCalendarException: Couldn't parse iCalendar
    at at.bitfire.ical4android.ICalendar$Companion.fromReader(ICalendar.kt:161)
    at at.bitfire.ical4android.Event$Companion.eventsFromReader(Event.kt:8)
…
Caused by: net.fortuna.ical4j.data.ParserException: Error at line 22:TimeZone is not applicable to current value
    at net.fortuna.ical4j.data.CalendarParserImpl.parse(CalendarParserImpl.java:17)
    at net.fortuna.ical4j.data.CalendarBuilder.build(CalendarBuilder.java:3)
    at net.fortuna.ical4j.data.CalendarBuilder.build(CalendarBuilder.java:2)
    at at.bitfire.ical4android.ICalendar$Companion.fromReader(ICalendar.kt:59)
    ... 37 more
Caused by: java.lang.UnsupportedOperationException: TimeZone is not applicable to current value
    at net.fortuna.ical4j.model.property.DateListProperty.setTimeZone(DateListProperty.java:64)
    at net.fortuna.ical4j.data.DefaultContentHandler.resolveTimezones(DefaultContentHandler.java:63)
    at net.fortuna.ical4j.data.DefaultContentHandler.endCalendar(DefaultContentHandler.java:1)
    at net.fortuna.ical4j.data.CalendarParserImpl.parseCalendar(CalendarParserImpl.java:50)
    at net.fortuna.ical4j.data.CalendarParserImpl.parseCalendarList(CalendarParserImpl.java:15)
    at net.fortuna.ical4j.data.CalendarParserImpl.parse(CalendarParserImpl.java:13)
    ... 40 more

I think it's because a TZID was applied to a DateListProperty without dates, something like RDATE;TZID=Some/TZ:. Unfortunately we don't have the iCalendar (I have requested it, but little hope to actually get it) and I can't reproduce with this test:

class Ical4jTest {

    @Test
    fun testDateList_WithoutDates_WithTZ() {
        val cal = ICalendar.fromReader(StringReader("BEGIN:VCALENDAR\r\n" +
                "VERSION:2.0\r\n" +
                "BEGIN:VEVENT\r\n" +
                "SUMMARY:Test\r\n" +
                "DTSTART;TZID=Europe/Vienna:20230824T161334\r\n" +
                "RDATE;TZID=Europe/Vienna:\r\n" +
                "END:VEVENT\r\n" +
                "END:VCALENDAR"))
        val event = cal.getComponent<VEvent>(Component.VEVENT)
        assertEquals("Test", event.summary.value)

        val rDate = event.getProperty<RDate>(Property.RDATE)
        assertNull(rDate.dates)
    }

}

Here we see that dates is not null, but an empty array, and then the code that causes the exception doesn't make any problems:

https://github.com/ical4j/ical4j/blob/527c76a34456e5916ed2efc8f2c960ddba8e2790/src/main/java/net/fortuna/ical4j/model/property/DateListProperty.java#L130-L133

ArnyminerZ commented 1 year ago

Updating

TLDR; no way to reproduce yet

After some tests without success, I will keep here everything I find.

The only way that DateListProperty doesn't get any value for dates is for constructor

/**
 * @param name       the property name
 * @param parameters property parameters
 */
public DateListProperty(final String name, final ParameterList parameters, PropertyFactory factory) {
    super(name, parameters, factory);
}

at RDate, this constructor is called here:

/**
 * @param aList  a list of parameters for this component
 * @param aValue a value string for this component
 * @throws ParseException where the specified value string is not a valid date-time/date representation
 */
public RDate(final ParameterList aList, final String aValue)
        throws ParseException {
    super(RDATE, aList, new Factory());
    periods = new PeriodList(false, true);
    setValue(aValue);
}

However, this constructor calls setValue, so it's quite improbable that dates doesn't get initialized.

After looking at this, there's an empty constructor in RDate that doesn't initialize anything for dates:

/**
 * Default constructor.
 */
public RDate() {
    super(RDATE, new Factory());
    periods = new PeriodList(false, true);
}

Then, if we do

@Test
fun testRDate_withoutDates() {
    val rDate = RDate()
    rDate.timeZone = tzReg.getTimeZone("Europe/Madrid")
}

nothing happens. This is because internally this constructor calls an initialization of PeriodList with

public PeriodList(boolean utc, final boolean unmodifiable) {
    this.utc = utc;
    this.unmodifiable = unmodifiable;
    if (unmodifiable) {
     periods = Collections.emptySet();
    }
    else {
     periods = new TreeSet<Period>();
    }
}

unmodifiable set to true, which initializes PeriodList.periods, that then it's used in RDate:

@Override
public final void setTimeZone(TimeZone timezone) {
    if (periods != null && !(periods.isEmpty() && periods.isUnmodifiable())) {
        periods.setTimeZone(timezone);
    } else {
        super.setTimeZone(timezone);
    }
}

So the problematic setTimeZone is not called here 😢. To sum up, I can't find a way to build an invalid RDate, so back to the drawing board.

After seeing this overridden method, I've realized that ExDate doesn't override anything, so maybe the issue is here. However, this test is also not-problematic:

@Test
fun testRDate_withoutDates() {
    val rDate = ExDate()
    rDate.timeZone = tzReg.getTimeZone("Europe/Madrid")
}

This is because the empty constructor calls the DateListProperty(final String name, PropertyFactory factory) constructor, which then initializes dates with

this(name, new DateList(Value.DATE_TIME), factory);

Again, so way dates is null.

Now I feel stupid for not having tried passing a null DateList. If this is called, the exception is thrown, so we have to find somewhere that dates might have been null.

@Test
fun testRDate_withoutDates() {
    val list: DateList? = null
    val exDate = ExDate(list)
    exDate.timeZone = tzReg.getTimeZone("Europe/Madrid")
}

which btw also fails with RDate, obviously. To search for this, it's important to note that there no way calling setValue or constructing with Strings doesn't end with a null dates, so we have to stick with other constructors.

I can't find anyhere that could be initializing with a null DateList

ArnyminerZ commented 1 year ago

@rfc2822 I've tried everything I can think of... Isn't there any way to get the original ical?

rfc2822 commented 1 year ago

@rfc2822 I've tried everything I can think of... Isn't there any way to get the original ical?

I have requested it, but I doubt the person is able to fetch & send it… we will see.

I'll leave it open for now, maybe we really get the ICS or manage to somehow reproduce it.

rfc2822 commented 1 year ago

Missing information, reopen when occurring again.